I'm trying to build a simple api in NestJs with authentication to save recipes to a MongoDB.
I was trying to add an email service to send a confirmation email for new users and ran into a dependency error I'm not able to figure out myself.
The error in question:
Error: Nest can't resolve dependencies of the UserService (?). Please make sure that the argument UserModel at index [0] is available in the EmailModule context.
Potential solutions:
- Is EmailModule a valid NestJS module?
- If UserModel is a provider, is it part of the current EmailModule?
- If UserModel is exported from a separate #Module, is that module imported within EmailModule?
#Module({
imports: [ /* the Module containing UserModel */ ]
})
at Injector.lookupComponentInParentModules (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:241:19)
at Injector.resolveComponentInstance (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:194:33)
at resolveParam (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:116:38)
at async Promise.all (index 0)
at Injector.resolveConstructorParams (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:131:27)
at Injector.loadInstance (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:57:13)
at Injector.loadProvider (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\injector.js:84:9)
at async Promise.all (index 4)
at InstanceLoader.createInstancesOfProviders (C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\instance-loader.js:47:9)
at C:\Users\Jonathan\Documents\Repos\pantry-api\node_modules\#nestjs\core\injector\instance-loader.js:32:13
It states the UserModel is missing in the EmailModule but that doesn't seem to be the case.
EmailModule:
import { Module } from "#nestjs/common";
import { ConfigModule } from "#nestjs/config";
import { UserModule } from "src/user/user.module";
import { JwtService } from "#nestjs/jwt";
import { UserService } from "src/user/user.service";
#Module({
imports: [ConfigModule, UserModule],
controllers: [],
providers: [JwtService, UserService],
})
export class EmailModule {}
Email Service:
import { Injectable } from "#nestjs/common";
import { ConfigService } from "#nestjs/config";
import { createTransport } from "nodemailer";
import * as Mail from "nodemailer/lib/mailer";
#Injectable()
export default class EmailService {
private nodemailerTransport: Mail;
constructor(private readonly configService: ConfigService) {
this.nodemailerTransport = createTransport({
service: configService.get("EMAIL_SERVICE"),
auth: {
user: configService.get("EMAIL_USER"),
pass: configService.get("EMAIL_PASSWORD"),
},
});
}
sendMail(options: Mail.Options) {
return this.nodemailerTransport.sendMail(options);
}
}
Email Confirmation Service:
import { Injectable } from "#nestjs/common";
import { JwtService } from "#nestjs/jwt";
import { ConfigService } from "#nestjs/config";
import EmailService from "./email.service";
import { UserService } from "src/user/user.service";
import { AccountStatus } from "src/types";
import { BadRequestException } from "#nestjs/common/exceptions";
interface VerificationTokenPayload {
email: string;
}
#Injectable()
export class EmailConfirmationService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private emailService: EmailService,
private userService: UserService,
) {}
sendVerificationLink(email: string) {
const payload: VerificationTokenPayload = { email };
const token = this.jwtService.sign(payload, {
secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
expiresIn: `${this.configService.get("JWT_VERIFICATION_TOKEN_EXPIRATION_TIME")}s`,
});
const url = `${this.configService.get("EMAIL_CONFIRMATION_URL")}?token=${token}`;
const text = `Welcome to Pantry! To confirm the email address, click here: ${url}`;
return this.emailService.sendMail({
to: email,
subject: "Pantry Account Confirmation",
text,
});
}
async confirmEmail(email: string) {
const user = await this.userService.findOne(email);
if (user && user.status !== AccountStatus.Created)
throw new BadRequestException("Email already confirmed");
await this.userService.markEmailAsConfirmed(email);
}
async decodeConfirmationToken(token: string) {
try {
const payload = await this.jwtService.verify(token, {
secret: this.configService.get("JWT_VERIFICATION_TOKEN_SECRET"),
});
if (typeof payload === "object" && "email" in payload) {
return payload.email;
}
throw new BadRequestException();
} catch (error) {
if (error?.name === "TokenExpiredError") {
throw new BadRequestException("Email confirmation token expired");
}
throw new BadRequestException("Bad confirmation token");
}
}
public async resendConfirmationLink(email: string) {
const user = await this.userService.findOne(email)
if (user.status === AccountStatus.Confirmed) {
throw new BadRequestException('Email already confirmed');
}
await this.sendVerificationLink(user.email);
}
}
I will add the other services & modules below in case they are of any use.
User Module:
import { Module } from "#nestjs/common";
import { MongooseModule } from "#nestjs/mongoose";
import { User, UserSchema } from "./schemas/user.schema";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
#Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
controllers: [UserController],
providers: [UserService]
})
export class UserModule {}
User Service:
import { BadRequestException, Injectable } from "#nestjs/common";
import { NotFoundException } from "#nestjs/common/exceptions";
import { InjectModel } from "#nestjs/mongoose";
import { Model, isValidObjectId } from "mongoose";
import { AccountStatus } from "src/types";
import { User, UserDocument } from "./schemas/user.schema";
import { MSG_USER_NOT_FOUND } from "./user-messages";
#Injectable()
export class UserService {
constructor(#InjectModel(User.name) private userModel: Model<UserDocument>) {}
private readonly defaultProjection = {
__v: false,
password: false,
};
async findOne(email: string): Promise<User> {
const user = this.userModel.findOne({ email }, this.defaultProjection);
if (user === null) throw new NotFoundException(MSG_USER_NOT_FOUND);
return user;
}
async deleteOne(id: string): Promise<any> {
if (!isValidObjectId(id)) throw new BadRequestException();
const result = await this.userModel.deleteOne({ _id: id }).exec();
if (result.deletedCount !== 1) throw new NotFoundException(MSG_USER_NOT_FOUND);
return result;
}
async updateOne(id: string, userData: User) {
if (!isValidObjectId(id)) throw new BadRequestException();
let result;
try {
result = await this.userModel.findByIdAndUpdate(id, userData).setOptions({ new: true });
} catch (e) {
throw new BadRequestException();
}
if (result === null) throw new NotFoundException(MSG_USER_NOT_FOUND);
return result;
}
async markEmailAsConfirmed(email: string) {
const user = await this.findOne(email);
return this.updateOne(user.email, {...user, status: AccountStatus.Confirmed})
}
}
Auth Module:
import { Module } from "#nestjs/common";
import { ConfigService } from "#nestjs/config";
import { JwtModule } from "#nestjs/jwt";
import { MongooseModule } from "#nestjs/mongoose";
import { PassportModule } from "#nestjs/passport";
import { EmailModule } from "src/email/email.module";
import { EmailConfirmationService } from "src/email/emailConfirmation.service";
import { User, UserSchema } from "src/user/schemas/user.schema";
import { UserModule } from "src/user/user.module";
import { UserService } from "src/user/user.service";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { LocalStrategy } from "./local.auth";
#Module({
imports: [
UserModule,
EmailModule,
PassportModule,
JwtModule.register({ secret: "secretKey", signOptions: { expiresIn: "10m" } }),
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
providers: [
AuthService,
JwtStrategy,
UserService,
LocalStrategy,
EmailConfirmationService,
ConfigService,
],
controllers: [AuthController],
})
export class AuthModule {}
Auth Service:
import { Injectable, NotAcceptableException, BadRequestException } from "#nestjs/common";
import { JwtService } from "#nestjs/jwt";
import { InjectModel } from "#nestjs/mongoose";
import * as bcrypt from "bcrypt";
import {
MSG_USER_EMAIL_TAKEN,
MSG_USER_NAME_TAKEN,
MSG_USER_NOT_FOUND,
MSG_USER_WRONG_CRED,
} from "src/user/user-messages";
import { UserService } from "../user/user.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
import { Model } from "mongoose";
import { User, UserDocument } from "src/user/schemas/user.schema";
import { AccountStatus } from "src/types";
#Injectable()
export class AuthService {
constructor(
#InjectModel(User.name) private userModel: Model<UserDocument>,
private userService: UserService,
private jwtService: JwtService,
) {}
async validateUser({ email, password }: LoginDto) {
const user = await this.userService.findOne(email);
if (!user) throw new NotAcceptableException(MSG_USER_NOT_FOUND);
const passwordValid = await bcrypt.compare(password, user.password);
if (user && passwordValid) return user;
return null;
}
async register({ email, username, password }: RegisterDto) {
const userWithEmail = await this.userModel.findOne({ email });
if (userWithEmail) throw new BadRequestException(MSG_USER_EMAIL_TAKEN);
const userWithName = await this.userModel.findOne({ username });
if (userWithName) throw new BadRequestException(MSG_USER_NAME_TAKEN);
const createdUser = new this.userModel({email, username, password});
createdUser.status = AccountStatus.Created;
const newUser = await createdUser.save();
return newUser;
}
async login(login: LoginDto) {
const user = await this.validateUser(login);
if (!user) throw new NotAcceptableException(MSG_USER_WRONG_CRED);
const payload = { email: user.email, sub: user._id };
return {
access_token: this.jwtService.sign(payload),
};
}
}
I hope this is enough to get some help, it's hard for me to share the entire repository at this point since it's work related
as you're trying to use UserService in another module other than where it was registered (which was UserModule), you need to expose it, like this:
#Module({
imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
controllers: [UserController],
providers: [UserService]
exports: [UserService], // <<<<
})
export class UserModule {}
then remove UserService from EmailModule.
Related
I've followed the official Nest doc (https://docs.nestjs.com/security/authentication) step by step, but I can't get validate() method called when using #AuthGuard('local') or #AuthGuard(LocalAuthGuard) on login action.
If I don't use that guard decorator, all works as expected (but I need to use it to get my token added to request object).
auth.controller.ts
#UseGuards(AuthGuard('local')) // or AuthGuard(LocalAuthGuard)
#Post('login')
async login(
#Request() req
) {
const { access_token } = await this.authService.login(req.user);
return access_token;
}
}
local.strategy.ts
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> { // class is constructed but this method is never called
const user: UserDto = await this.authService.login({
email,
password,
});
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
auth.module.ts
#Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: "bidon",
signOptions: {
expiresIn: '3600',
},
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, PassportModule, JwtModule],
controllers: [AuthController],
})
export class AuthModule {}
PS : I've already read all stack overflow related posts (for exemple : NestJS' Passport Local Strategy "validate" method never called) but they didn't help me.
I found that if we don't pass email or password, also the wrong value of both, the guard will response Unauthorized message. The problem is how to ensure that validation of the required field before run guard logic if it not defined, In other words, frontend not pass it to server. If we add #Body() data: loginDto in controller method, it won't validate the body params.
to solve it, I add some validation code in local.guard.ts file. Here is my code in my project:
import { HttpException, HttpStatus, Injectable, UnauthorizedException } from "#nestjs/common";
import { AuthGuard } from "#nestjs/passport";
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
handleRequest(err, user, info, context, status) {
const request = context.switchToHttp().getRequest();
const { mobile, password } = request.body;
if (err || !user) {
if (!mobile) {
throw new HttpException({ message: '手机号不能为空' }, HttpStatus.OK);
} else if (!password) {
throw new HttpException({ message: '密码不能为空' }, HttpStatus.OK);
} else {
throw err || new UnauthorizedException();
}
}
return user;
}
}
ValidationPipe doesn't validate your request. Because, Gurads are executed before any interceptor or pipe. But, guards are executed after middleware. So, we can create a validation middleware to solve this issue. Here is my solution. Hope it will help somebody.
login.dto.ts
import { ApiProperty } from '#nestjs/swagger';
import { IsEmail, IsNotEmpty } from 'class-validator';
export class LoginDto {
#ApiProperty({ required: true })
#IsNotEmpty()
#IsEmail()
username: string;
#ApiProperty({ required: true })
#IsNotEmpty()
password: string;
}
authValidationMiddleware.ts
import {
Injectable,
NestMiddleware,
BadRequestException,
} from '#nestjs/common';
import { Response, NextFunction } from 'express';
import { validateOrReject } from 'class-validator';
import { LoginDto } from '../dto/login.dto';
#Injectable()
export class AuthValidationMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const body = req.body;
const login = new LoginDto();
const errors = [];
Object.keys(body).forEach((key) => {
login[key] = body[key];
});
try {
await validateOrReject(login);
} catch (errs) {
errs.forEach((err) => {
Object.values(err.constraints).forEach((constraint) =>
errors.push(constraint),
);
});
}
if (errors.length) {
throw new BadRequestException(errors);
}
next();
}
}
auth.module.ts
import { MiddlewareConsumer, RequestMethod } from '#nestjs/common';
import { AuthController } from './auth.controller';
import { AuthValidationMiddleware } from './middleware/authValidationMiddleware';
#Module({
imports: ['.. imports'],
controllers: [AuthController],
})
export class AuthModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthValidationMiddleware)
.forRoutes({ path: 'auth/login', method: RequestMethod.POST });
}
}
This local strategy expects your body to have username and password fields, on your code change email to username
When you use NestJs Guard then it executed before Pipe therefore ValidationPipe() doesn't validate your request.
https://docs.nestjs.com/guards
Guards are executed after all middleware, but before any interceptor or pipe.
My use case requires only one parameter.
import { Injectable, UnauthorizedException, BadRequestException } from '#nestjs/common'
import { PassportStrategy } from '#nestjs/passport'
import { Request } from 'express'
import { Strategy } from 'passport-custom'
import { AuthService } from '../auth.service'
#Injectable()
export class CustomStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super()
}
async validate(req: Request): Promise<any> {
// req.body.xxx can verify any parameter
if (!req.body.code) {
throw new BadRequestException('Missing code parameters!')
// Using the above, this is how the response would look:
// {
// "message": "Missing code parameters!",
// "error": "Bad Request",
// "statusCode": 400,
// }
}
const user = await this.authService.validateUser(req.body.code)
console.log('user', user)
if (!user) {
throw new UnauthorizedException()
}
return user
}
}
here is my auth.controller.ts
import { Body, Controller, Get, Post, Req, Res, UnauthorizedException, ValidationPipe } from '#nestjs/common';
import { ApiBadRequestResponse, ApiCreatedResponse, ApiForbiddenResponse, ApiOkResponse, ApiTags, ApiUnauthorizedResponse } from '#nestjs/swagger';
import { User } from 'src/interfaces/user.interface';
import { AuthService } from './auth.service';
import { AuthCredentials } from './dto/auth-credential.dto';
import { Response,Request } from 'express';
import { JwtService } from '#nestjs/jwt';
#ApiTags('auth')
#Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private jwtService: JwtService
){}
#Post('/signup')
#ApiCreatedResponse({description: 'this response has created successfully'})
#ApiForbiddenResponse({description: 'Forbidden'})
signUp(#Body(ValidationPipe) authCredentials:AuthCredentials):Promise<User> {
return this.authService.signUp(authCredentials);
}
#Post('/signin')
#ApiOkResponse({description:'The resource has been successfully returned'})
#ApiForbiddenResponse({description:'Invalid credintials'})
async signin(#Body(ValidationPipe) authCredentials:AuthCredentials,#Res() response: Response):Promise<{accessToken:string}> {
const token = await this.authService.signIn(authCredentials);
response
.cookie('access_token', token, {
httpOnly: true,
domain: 'localhost', // your domain here!
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
// expires: new Date(Date.now() + 60000 ),
})
.send(token);
return token;
}
#Get('/cookies')
findAll(#Req() request: Request) {
console.log(request.cookies['access_token'].accessToken);
// or
// "request.cookies['cookieKey']"
// or
// console.log(request.signedCookies);
}
// to verify the user's token
#Get('/user')
#ApiOkResponse({description:'User varified Successfully'})
#ApiUnauthorizedResponse({description:'Unauthorised User'})
async user(#Req() request: Request) {
try{
const cookies = request.cookies['access_token']
const cookie = cookies.accessToken
const data = await this.jwtService.verifyAsync(cookie)
if(!data) {
throw new UnauthorizedException()
}
const user = await this.authService.findOne(data.username)
return user;
}catch(e){throw new UnauthorizedException()}
}
// to logout by delelting the token
#ApiOkResponse({description:'User varified Successfully'})
#ApiBadRequestResponse({description:'login failed'})
#Post('logout')
async logout(#Res({passthrough: true}) response: Response) {
response.clearCookie('access_token');
return {
message: 'Logged out successfully'
}
}
}
here is my auth.module.ts
import { Module } from '#nestjs/common';
import { APP_GUARD } from '#nestjs/core';
import { JwtModule } from '#nestjs/jwt';
import { PassportModule } from '#nestjs/passport';
import { DatabaseModule } from 'src/database/database.module';
import { AuthController } from './auth.controller';
import { authProviders } from './auth.provider';
import { AuthService } from './auth.service';
import { AuthStrategy } from './auth.strategy';
import { JwtStrategy } from './jwt.strategy';
import { RolesGuard } from './roles.guard';
import { UserRepository } from './user.repository';
#Module({
imports: [
PassportModule.register({defaultStrategy:'jwt'}),
JwtModule.register({
secret: 'abcdefghijklmnop',
signOptions:{
expiresIn: 60000,
}
}),
DatabaseModule],
controllers: [AuthController],
providers: [
...authProviders,
AuthService,
UserRepository,
JwtStrategy,
AuthStrategy,
{
provide: APP_GUARD,
useClass: RolesGuard,
},
// RolesGuard
],
exports: [
JwtStrategy,
PassportModule
]
})
export class AuthModule {}
here is jwt.strategy.ts
import { PassportStrategy } from "#nestjs/passport";
import { Strategy,ExtractJwt } from "passport-jwt"
import { Inject, Injectable, UnauthorizedException } from "#nestjs/common";
import { JwtPayload } from "../interfaces/jwt-payload.interface";
import { Model } from "mongoose";
import { User } from "src/interfaces/user.interface";
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
#Inject('USER_MODEL')
private authModel: Model<User>,
){
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken('Bearer'),
secretOrKey: 'abcdefghijklmnop',
})
}
async validate(payload: JwtPayload):Promise<User>{
const { username } = payload ;
const user = await this.authModel.findOne({ username }) ;
if(!user){
throw new UnauthorizedException()
}
return user
}
// async validate(payload: any) {
// return { ...payload.user };
// }
}
here is my user.repository.ts
import { BadRequestException, Inject, Injectable, UnauthorizedException } from "#nestjs/common";
import { Model } from "mongoose";
import { User } from "src/interfaces/user.interface";
import { AuthCredentials } from "./dto/auth-credential.dto";
var bcrypt = require('bcryptjs');
#Injectable()
export class UserRepository{
constructor(
#Inject('USER_MODEL')
private authModel: Model<User>
){}
async validateUser(authCredentials):Promise<boolean> {
const username = authCredentials.username
const userExist = await this.authModel.findOne({ username})
if(userExist === null) {return true}
else {return false}
}
async validateUserPassword(authCredentials:AuthCredentials): Promise<string> {
const { username, password } = authCredentials;
const user = await this.authModel.findOne({ username})
if (user === null) {
throw new UnauthorizedException('Invalid credintials')
}else {
const isMatch = await bcrypt.compare(password, user.password)
if (isMatch) {
return user.username
}else {throw new BadRequestException('Invalid credintials')}
}
}
}
this is auth module. I've applicant, application and some other modules too which I've to authenticate. These are working fine in swagger but I've a react frontend app, Which also working partially, means it only storing the token in the cookie but whenever trying to get or access other modules they responses as 401(Unauthorised)
I am in the process of working out a custom validator to be used to verify if a value exists in the database
I managed to get the code to work by using getConnection() from the typeorm package. But I would ideally want to inject the typeorm connection object constructor(private connection: Connection) {} this gives me more flexibility with the testing.
import { BadRequestException, Injectable } from '#nestjs/common';
import {
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { Connection, getConnection } from 'typeorm';
#ValidatorConstraint({ name: 'ExistsRule', async: true })
#Injectable()
export class ExistsRule implements ValidatorConstraintInterface {
constructor(private connection: Connection) {}
async validate(value: number | string, args: ValidationArguments) {
const { constraints } = args;
if (constraints.length === 0) {
throw new BadRequestException(`Failed validating ${value} exists.`);
}
const str = constraints[0].split(':');
const tableName = str[0];
const columnName = str[1];
const result = await getConnection().query(
`select count(*) from ${tableName} where ${columnName}=$1`,
[value],
);
// The record already exists. Failing the validation.
if (result[0].count > 0) {
return false;
}
return true;
}
defaultMessage(args: ValidationArguments) {
const { property, value } = args;
return `${property} ${value} is already taken.`;
}
}
DTO that is using this validator. (For completeness)
import {
IsEmail,
IsNotEmpty,
IsPhoneNumber,
IsString,
Validate,
} from 'class-validator';
import { ExistsRule } from 'src/customer/exists.validator';
export class CreateCustomerDto {
#IsString()
firstName: string;
#IsString()
lastName: string;
#IsEmail()
#Validate(ExistsRule, ['customer:email'])
email: string;
#IsPhoneNumber()
mobileNumber: string;
#IsNotEmpty()
password: string;
}
#Module({
imports: [
TypeOrmModule.forFeature([Customer]),
JwtModule.register({
secret: 'secret-to-be-put-in-the-env',
signOptions: { expiresIn: '300s' },
}),
CommonModule,
],
controllers: [BookingsController, AuthController],
providers: [CustomerService, AuthService, LocalStrategy, JwtStrategy],
})
export class CustomerModule {}
import { Module } from '#nestjs/common';
import { UniqueRule } from './unique.validator';
#Module({
providers: [UniqueRule],
})
export class CommonModule {}
Thanks to #JayMcDoniel's comments, and guidence. The solution was to edit main.ts and add the useContainer from class-validator module to instruct it to use the nestjs container.
import { ValidationPipe } from '#nestjs/common';
import { NestFactory } from '#nestjs/core';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
import { CommonModule } from './common/common.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
}),
);
useContainer(app.select(CommonModule), { fallbackOnErrors: true });
await app.listen(3000);
}
bootstrap();
I am trying to add role guard in Nest JS API. I used Passport, Jwt authentication for this.
In my RolesGuard class I made the request and get the user from it to check the user role valid or not. I attached the code below.
roles.guard.ts
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user: User = request.user;
return this.userService.findOne(user.id).pipe(
map((user: User) => {
const hasRole = () => roles.indexOf(user.role) > -1;
let hasPermission: boolean = false;
if (hasRole()) {
hasPermission = true;
}
return user && hasPermission;
}),
);
}
Problem here is context.switchToHttp().getRequest() returns object, which is undefined. So I could not get user details from it.
After I had some research about this error I found that order of the decorators in controller can be the issue. Then I changed the order, but still problem appears as same. Bellow I added that code also.
user.controller.ts
#UseGuards(JwtAuthGuard, RolesGuard)
#hasRoles(UserRole.USER)
#Get()
findAll(): Observable<User[]> {
return this.userService.findAll();
}
-Thank you-
if you are using graphql you can make the changes to fit the code below
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { GqlExecutionContext } from '#nestjs/graphql';
import { Role } from 'src/enums/role.enum';
import { ROLES_KEY } from '../auth';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const ctx = GqlExecutionContext.create(context);
const user = ctx.getContext().req.user;
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
if you are not using graphql, make the changes to fit the code below
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
then in you controller you can do
#UseGuards(JwtAuthGuard, RolesGuard)
#hasRoles(UserRole.USER)
#Get()
findAll(): Observable<User[]> {
return this.userService.findAll();
}
finally, for all this to work add the global guard to your app.module.ts
like so
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import {MongooseModule} from '#nestjs/mongoose';
import { AppService } from './app.service';
import { ConfigModule } from '#nestjs/config';
import configuration from 'src/config/configuration';
import { RolesGuard } from 'src/auth/auth';
import { UsersModule } from '../users/users.module';
import { AdminsModule } from 'src/admin/admins.module';
import { config } from 'dotenv';
import * as Joi from 'joi';
config();
#Module({
imports: [
// other modules
UsersModule,
// configuration module
ConfigModule.forRoot({
isGlobal: true,
cache: true,
load: [configuration],
expandVariables: true,
// validate stuff with Joi
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
PORT: Joi.number().default(5000),
}),
validationOptions: {
// allow unknown keys (change to false to fail on unknown keys)
allowUnknown: true,
abortEarly: true,
},
}),
// connect to mongodb database
MongooseModule.forRoot(process.env.DB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
}),
],
controllers: [AppController],
providers: [
AppService,
{
provide: 'APP_GUARD',
useClass: RolesGuard,
}
],
})
export class AppModule {}
I have a user service that handles registration, login and some other functions. I have been working on the application for sometime now with over 10 modules. But I noticed each time I injected another service and I try to consume any endpoint I get this error "Unknown authentication strategy "jwt". Error on swagger is:-
Internal Server Error
Response body
{
"statusCode": 500,
"message": "Internal server error"
}
Once I remove the injected service from the user module, everything is fine again. I have been trying to fix this because I need to use this service inside the user module.
This is the jwt.Strategy.ts
import { Injectable, HttpException, HttpStatus } from "#nestjs/common";
import { PassportStrategy } from '#nestjs/passport';
import { Strategy, ExtractJwt, VerifiedCallback } from "passport-jwt";
import { AuthService } from './auth.service';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.SECRETKEY
});
}
async validate(payload: any, done: VerifiedCallback) {
const user = await this.authService.validateUser(payload);
try {
if (user) {
//return user;
return done(null, user, payload.iat)
} else if (user == null) {
const Terminal = await this.authService.validatePos(payload);
return done(null, Terminal, payload.iat)
}
else {
return done(
//throw new UnauthorizedException();
new HttpException('Unauthorised access', HttpStatus.UNAUTHORIZED),
false,
);
}
} catch (error) {
return error;
}
}
}
This is the AuthModule
import { Module } from '#nestjs/common';
import { AuthService } from './auth.service';
import { UserService } from '../user/user.service';
import { UserSchema } from '../user/user.schema';
import { MongooseModule } from '#nestjs/mongoose';
import { JwtStrategy } from './jwt.strategy';
import { ActivityLogService } from '../activity-log/activity-log.service';
import { ApiKeyStrategy } from './apiKey.strategy';
import { PassportModule } from "#nestjs/passport";
#Module({
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }]),
PassportModule.register({
secret: "mysec"
}),
ActivityLogService],
providers: [AuthService, UserService, JwtStrategy, ApiKeyStrategy, ActivityLogService],
exports: [AuthService],
controllers: []
})
export class AuthModule { }
This is the api.strategy.ts
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, HttpException, HttpStatus } from '#nestjs/common';
import { AuthService } from './auth.service';
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(private authService: AuthService) {
super({
header: 'api_key',
prefix: ''
}, true,
(apikey: string, done: any, req: any, next: () => void) => {
const checkKey = this.authService.validateApiKey(apikey);
if (!checkKey) {
return done(
new HttpException('Unauthorized access, verify the token is correct', HttpStatus.UNAUTHORIZED),
false,
);
}
return done(null, true, next);
});
}
}
This is the authService
import { Injectable } from '#nestjs/common';
import { sign } from 'jsonwebtoken';
import { UserService } from '../user/user.service';
import { TerminalService } from '../terminal/terminal.service';
import { InjectModel } from '#nestjs/mongoose';
import { Model } from 'mongoose';
import { Terminal } from '../terminal/interfaces/terminal.interface';
#Injectable()
export class AuthService {
constructor(private userService: UserService, #InjectModel('Terminal') private terminalModel: Model<Terminal>,) { }
//generate token for user
async signPayLoad(payload: any) {
return sign(payload, process.env.SECRETKEY, { expiresIn: '1h' });
}
//find user with payload
async validateUser(payload: any) {
const returnuser = await this.userService.findByPayLoad(payload);
return returnuser;
}
//generate token for Posy
async signPosPayLoad(payload: any) {
return sign(payload, process.env.SECRETKEY, { expiresIn: '24h' });
}
//find terminal with payload
async validatePos(payload: any) {
const { terminalId } = payload;
const terminal = await this.terminalModel.findById(terminalId);
return terminal;
}
validateApiKey(apiKey: string) {
const keys = process.env.API_KEYS;
const apiKeys = keys.split(',');
return apiKeys.find(key => apiKey === key);
}
}
This is the user service
import { Injectable, HttpException, HttpStatus, Inject } from '#nestjs/common';
import { User } from './interfaces/user.interface';
import { Model } from 'mongoose';
import { InjectModel } from '#nestjs/mongoose';
import { LoginUserDto } from './login-user.dto';
import { ActivityLogService } from '../activity-log/activity-log.service';
import { UpdateUserDTO } from './dtos/update_user.dto';
#Injectable()
export class UserService {
constructor(#InjectModel('User') private userModel: Model<User>,
private activityLogService: ActivityLogService,
) { }
//Login user
private users = [
{
"userId": 1,
"name": "John Doe",
"username": "john",
"password": "john123"
},
]
async login(loginDTO: LoginUserDto) {
const { email, password } = loginDTO;
return await this.users.find(users => users.username == email)
}
async findByPayLoad(payload: any) {
const { userId } = payload;
return await this.userModel.findById(userId)
}
async getAllUser() {
return this.users;
}
}
I cant figure out what I am doing wrong
Besides the code being difficult to manipulate and determine what's happening, services being in places they shouldn't and re-instantiations of services all over the place, the culprit as to why you are getting the error is simply because you never register the PassportModule from #nestjs/passport
EDIT 1/7/2017
Coming back to this answer about a year and a half later, it looks like the real issue is the use of a REQUEST scoped provider in the strategy. The nest docs explicitly mention this can't be done directly but also have a workaround for it.