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
}
}
Related
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.
I need help with processing after authentication using Nest.js
here do I pass the failureRedirect option for passport-local when using Nest.js for authentication?
Without Nest.js
app.post('/login', passport.authenticate('local', {
//Passing options here.
successRedirect: '/',
failureRedirect: '/login'
}));
My code is. (with Nest.js)
local.strategy.ts
import { Injectable, UnauthorizedException } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import { Strategy } from "passport-local";
import { AuthService } from "./auth.service";
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
//I tried passing the option here. but failed.
})
}
async validate(username: string, password: string): Promise<string | null> {
const user = this.authService.validate(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
local.guard.ts
import { Injectable } from "#nestjs/common";
import { AuthGuard } from "#nestjs/passport";
#Injectable
export class LocalAuthGuard extends AuthGuard('local') {}
auth.controller.ts
import { Controller, Get, Post, Render, UseGuards } from "#nestjs/common";
import { LocalAuthGuard } from "./local.guard";
#Controller()
export class AuthController {
#Get("/login")
#Render("login")
getLogin() {}
//Redirect to '/login' when authentication failed.
#UseGuards(LocalAuthGuard)
#Post("/login")
postLogin() {}
}
auth.module.ts
import { Module } from "#nestjs/common";
import { PassportModule } from "#nestjs/passport";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { LocalStrategy } from "./local.strategy";
import { LocalAuthGuard } from "./local.guard";
#Module({
controllers: [AuthController],
imports: [PassportModule],
providers: [AuthService, LocalStrategy, LocalAuthGuard]
})
export class AuthModule {}
I tried adding code to AuthController#postLogin to redirect on login failure, but the code seems to run only on successful login.
I would like to redirect to the login page again in case of login failure with the failureRedirect option of passport-local.
I found a workaround since using the passport options sadly didn't work:
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
getAuthenticateOptions(context: ExecutionContext): IAuthModuleOptions {
return {
successReturnToOrRedirect: '/',
failureRedirect: '/login',
};
}
}
Instead I created a Nestjs Filter to catch an exception containing a redirect URL.
redirecting.exception.ts
export class RedirectingException {
constructor(public url: string) {}
}
redirecting-exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter } from '#nestjs/common';
import { Response } from 'express';
import { RedirectingException } from './redirecting.exception';
#Catch(RedirectingException)
export class RedirectingExceptionFilter implements ExceptionFilter {
catch(exception: RedirectingException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.redirect(exception.url);
}
}
In my validate method I'm throwing the RedirectingException with the correct error msg, e.g.
throw new RedirectingException('/login?error="User not found"');
And the controller handles the rest of the redirecting and passes the error to the view, so it can be displayed:
#Get('/login')
#Render('login.pug')
#Public()
async login(#Query() query) {
return { error: query.error };
}
#Post('/login')
#Public()
#UseGuards(LocalAuthGuard)
#Redirect('/')
async doLogin() {}
I'd rather use the passport functionality including the failureFlash, but I couldn't get it to work.
I have followed this guide in an attempt to get JWT authentication working.
The only difference I have is that I keep JWT Token in HttpOnly cookie which means a custom extractor is required.
I found an example of how to extract a Token from a cookie. So, the only difference is:
jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => {
return req?.cookies?.access_token
}])
Unfortunately, req is undefined for no apparent reason.
That's how my auth.module.ts looks like:
#Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'qweqweqweqeqwe',
signOptions: { expiresIn: '20s' }
})
],
providers: [
AuthService,
AuthResolver,
JwtAuthGuard,
JwtStrategy
]
})
export class AuthModule { }
I have also created a strategy file jwt.stragegy.ts:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => {
return req?.cookies?.access_token
}]),
ignoreExpiration: false,
secretOrKey: 'qweqweqweqeqwe',
})
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username }
}
}
auth.guard.ts:
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Could it be an error on passport.js library's side? Like, #nestjs/passport fails to map arguments or something...
By following the docs, you should be able to get the request.
import { ExecutionContext } from '#nestjs/common';
import { GqlExecutionContext } from '#nestjs/graphql';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
In my scenario what was making the payload.sub undefined was the login function on auth.service.
Before (not working):
async login(user: any) {
const payload = { apiKey: user.apiKey, sub: user.userId };
const token = this.jwtService.sign(payload);
return `Access token=${token};\n HttpOnly;\n Path=/;\n Max-
Age=${this.configService.get('jwtConstants.expirationTime')}`;
}
After changes (working):
async login(user: any) {
const payload = { apiKey: user._doc.apiKey, sub: user._doc._id };
const token = this.jwtService.sign(payload);
return `Access token=${token};\n HttpOnly;\n Path=/;\n Max-Ag
e=${this.configService.get('jwtConstants.expirationTime')}`;
}
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.
I have an existing authentication for users which is already working fine. The token for user authentication expires within an hour.
I want to implement another separate authentication strategy a third API that is consuming my Nestjs API. There are separate endpoints for the third-party API, the token should expire with 24 hours. The API has to stay connected to my app for 24 hours.
I don't mind using additional package to achieve this.
I also need to create a guard called thirdParty Guard so that the 3rd part API alone will have access to that endpoint.
This is my jwt.strategy.ts
#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);
if (!user) {
return done(
new HttpException('Unauthorised access', HttpStatus.UNAUTHORIZED),
false,
);
}
//return user;
return done(null, user, payload.iat)
}
}
ApiKey.strategy.ts
#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);
});
}
}
and this is the auth.service.ts
#Injectable()
export class AuthService {
constructor(private userService: UserService) { }
async signPayLoad(payload: any) {
return sign(payload, process.env.SECRETKEY, { expiresIn: '1h' });
}
async validateUser(payload: any) {
const returnuser = await this.userService.findByPayLoad(payload);
return returnuser;
}
validateApiKey(apiKey: string) {
const keys = process.env.API_KEYS;
const apiKeys = keys.split(',');
return apiKeys.find(key => apiKey === key);
}
}
With the above setup, If you are using Passport-HeaderAPIKey then try adding headerapikey in the Guard. The below code worked for me.
Ref: NestJS extending guard
import { ExecutionContext, Injectable } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { AuthGuard as NestAuthGuard } from '#nestjs/passport';
#Injectable()
export class AuthGuard extends NestAuthGuard(['jwt', 'headerapikey']) {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}