NestJs authentication with JWT strategy - add validation option of "ignoreNotBefore" - node.js

I am using an AuthGuard in NestJs to validate the requests jwt token.
Because of my service is only validate the token and not created it, It must not use the "nbf" validation in order to avoid cases the the time of the server which creates the token is later than my server.
When working with pure node.js using jsonwebtoken library it is easy to add option to turn off this validation by adding:
jwt.verify(token, cert, {ignoreNotBefore: true})
This is working as well.
But, how can I do it using nest?
This is my guard:
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector,
private authService: AuthService) {
super();
}
async canActivate(context: ExecutionContext) {
const isValid = await super.canActivate(context);
return isValid;
}
handleRequest(err, user, info) {
if (err || !user) {
Logger.error(`Unauthorized: ${info && info.message}`);
throw err || new UnauthorizedException();
}
return user;
}
}
In the JWT strategy, I tried to add the ignoreNotBefore option when calling "super" of PassportStrategy, bur this is not working:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService,
private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
ignoreNotBefore: true,
secretOrKey: fs.readFileSync(config.get('auth.secret')),
});
}
validate(payload: any) {
const isAuthorized = this.authConfig.roles.some((role) => payload.role?.includes(role));
if(!isAuthorized) {
Logger.error(`Unauthorized: Invalid role`);
throw new UnauthorizedException();
}
return true;
}
}
What is the right way to do that?
Thanks.

JwtAuthGuard
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService,
private config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
jsonWebTokenOptions: {
// this object maps to jsonwebtoken verifier options
ignoreNotBefore: true,
// ...
// maybe ignoreExpiration too?
},
secretOrKey: fs.readFileSync(config.get('auth.secret')),
});
}
validate(payload: any) {
const isAuthorized = this.authConfig.roles.some((role) => payload.role?.includes(role));
if(!isAuthorized) {
Logger.error(`Unauthorized: Invalid role`);
throw new UnauthorizedException();
}
return true;
}
}
Explanation
Move your ignoreNotBefore to jsonWebTokenOptions as this object maps to the jsonwebtoken verifier options. This is as Nest.js has wrapped passport-jwt and passport-jwt wraps jsonwebtoken. So options in the root object are mainly configuring the strategy (passport) and not configuring jsonwebtoken (as much).
Learn More
http://www.passportjs.org/packages/passport-jwt/

Related

refresh token API error "secretOrPrivateKey must have a value"

When a user logs into the API generates a token so that he has access to other endpoints, but the token expires in 60sec, I made a function to generate a new valid token using the old token (which was stored in the database), but when I'm going to generate a new valid token I'm getting the secretOrPrivateKey must have a value error
The function refreshToken use function login to generate a new token
Nest error:
secretOrPrivateKey must have a value
Error: secretOrPrivateKey must have a value
at Object.module.exports [as sign] (C:\Users\talis\nova api\myflakes_api\node_modules\jsonwebtoken\sign.js:107:20)
at JwtService.sign (C:\Users\talis\nova api\myflakes_api\node_modules\#nestjs\jwt\dist\jwt.service.js:28:20)
at AuthService.login (C:\Users\talis\nova api\myflakes_api\src\auth\auth.service.ts:18:39)
at TokenService.refreshToken (C:\Users\talis\nova api\myflakes_api\src\token\token.service.ts:39:37)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at TokenController.refreshToken (C:\Users\talis\nova api\myflakes_api\src\token\token.controller.ts:12:16)
at C:\Users\talis\nova api\myflakes_api\node_modules\#nestjs\core\router\router-execution-context.js:46:28
at C:\Users\talis\nova api\myflakes_api\node_modules\#nestjs\core\router\router-proxy.js:9:17
My code:
Function refreshToken in the file token.service.ts
async refreshToken(oldToken: string) {
let objToken = await this.tokenRepository.findOne({hash: oldToken})
if (objToken) {
let user = await this.userService.findOneOrFail({email:objToken.email})
return this.authService.login(user)
} else {
return new UnauthorizedException(MessagesHelper.TOKEN_INVALID)
}
}
Function login in the file auth.service.ts
async login(user: UsersEntity) {
const payload = { email: user.email, sub: user.idUser }
const token = this.jwtService.sign(payload) // here!!!
this.tokenService.save(token, user.email)
return {
token: token
};
}
Error is on const token = this.jwtService.sign(payload)
Here is the file jwt.strategy.ts
import { Injectable } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstants } from "../constants";
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: { sub: any; email: any; }) {
return { id: payload.sub, email: payload.email}
}
}
And here local.strategy.ts
import { Injectable, UnauthorizedException } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import { Strategy } from "passport-local";
import { MessagesHelper } from "src/helpers/messages.helper";
import { AuthService } from "../auth.service";
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> {
const user = await this.authService.validateUser(email, password);
if(!user)
throw new UnauthorizedException(MessagesHelper.PASSWORD_OR_EMAIL_INVALID)
return user;
}
}
this is the AuthModule where is JwtModule.register
#Module({
imports: [
ConfigModule.forRoot(),
UsersModule,
PassportModule,
TokenModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
controllers: [AuthController],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [JwtModule, AuthService]
})
export class AuthModule {}
Guys i tried to use images, but i'm new user and i still don't have a reputation, sorry.
Doing what #Micael Levi mentioned in the comments worked for me, so it would be:
const token = this.jwtService.sign(payload, jwtConstants.secret)
For future reference, I encountered this issue despite my environment variables being defined (process.env.SECRET_KEY being undefined was a common problem seen in other similar questions). So what I did to fix mine was:
return {
access_token: this.jwtService.sign(payload, { secret: process.env.JWT_SEC }),
};

Nestjs Passport-jwt better Unauthorized strategy

just going through docs on authentication in NestJS: docs.nestjs.com
Here is the code:
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable } from '#nestjs/common';
import { jwtConstants } from './constants';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
According to docs validate method is called when request contains jwt and that jwt is valid.
I am wondering is there a callback method for the case when jwt is missing from request header, or jwt is invalid or expired. I would like to return response error with message to client that their token is expired or missing...
Thanks
You could implement a custom strategy and check headers or cookies how you like. This is a (shortened) exmaple I'am using on my app.
import { JwtService } from '#nestjs/jwt';
import { Strategy } from 'passport-custom';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'custom-jwt') {
constructor(private readonly jwtService: JwtService) {
super();
}
async validate(req: Request): Promise<any> {
const token = req.cookies.auth ?? req.headers.authorization;
if (!token) {
throw new UnauthorizedException();
}
const user = this.jwtService.decode(token, {
json: true
});
if (!user) {
throw new UnauthorizedException();
}
if (this.isExpired(user)) {
throw new UnauthorizedException();
}
return user;
}
private isExpired(user: JwtUserDto): boolean {
// ...
}
}
This code checks for a jwt token either in a "auth"-cookie or in "Authorization"-header and by returning user, it attaches the decoded user (if valid) to the request.
To use it: export class JwtAuthGuard extends AuthGuard('custom-jwt')
It's just an example, to see how it works. You might need to adapt it to fit your needs.

LocalAuthGuard not working in nestjs app with typeorm and passport-local

I am using nestjs 8.0 with typeorm, passport-jwt, and passport-local. Everything seems to be working fine other than the LocalAuthGuard. I am able to successfully create a new user and even use the routes that have JwtAuthGuard but LocalAuthGuard seems to have some issues as I keep getting 401 Unauthorized error
Also, is there a way to console log some output from within the LocalAuthGuard or LocalStrategy?
auth.controller.ts
#Controller(['admin', 'user'])
export class AuthController {
constructor(
private authService: AuthService,
) {}
#UseGuards(LocalAuthGuard)
#Post('login')
login(#Request() req) {
console.log('object');
if (req.path.includes('admin') && !req.user.isAdmin) {
throw new UnauthorizedException();
}
return this.authService.login(req.user);
}
...
}
local.guard.ts
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
local.strategy.ts
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(usernameOrEmail: string, password: string): Promise<any> {
const user = await this.authService.validateUser({
usernameOrEmail,
password,
});
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
auth.service.ts
#Injectable()
export class AuthService {
constructor(
#InjectRepository(User)
private userRepository: Repository<User>,
private jwtService: JwtService,
) {}
async validateUser({ usernameOrEmail, password }: LoginDto) {
const user = (await this.userRepository.findOne({ username: usernameOrEmail })) ||
(await this.userRepository.findOne({ email: usernameOrEmail }));
if (user && (await bcrypt.compare(password, user.password))) {
return user;
}
return null;
}
...
}
auth.module.ts
#Module({
imports: [
TypeOrmModule.forFeature([User]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get('JWT_KEY'),
signOptions: {
expiresIn: '6000s',
},
};
},
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
Any help or suggestion is greatly appreciated.
EDIT
It seems for LocalAuthGuard, username, and password are a must and other properties are optional.
You can create multiple local strategies with different parameters. For example,
Username and password
Phone and OTP
Email and password
Email and OTP
For using multiple local strategies, refer to this answer
Then, you can also pass an options object to specify different property names, for example: super({ usernameField: 'email', passwordField: 'otp' })
implement local strategy as follows
import { Strategy } from 'passport-local';
import { PassportStrategy } from '#nestjs/passport';
import { Inject, Injectable, UnauthorizedException } from '#nestjs/common';
import { IAuthService } from '../services';
import { LoginDto, OtpLoginDto } from '../dto';
import { UserDto } from 'src/modules/user/dto';
import { plainToClass } from 'class-transformer';
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(
#Inject(IAuthService)
private readonly authService: IAuthService,
) {
super({
usernameField: 'user_name',
passwordField: 'otp',
});
}
async validate(user_name: string, otp: string): Promise<any> {
const loginDto = plainToClass(OtpLoginDto, {
username: user_name,
otp: otp,
});
const user: UserDto = await this.authService.verifyOtp(loginDto);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
ref: customize passport

How to get payload from jwt strategy?

I have jwt strategy:
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
constructor() {
super({
ignoreExpiration: false,
secretOrKey: "secret",
jwtFromRequest: ExtractJwt.fromExtractors([
(request: Request) => {
let data = request.cookies['access'];
return data;
}
]),
});
}
async validate(payload: any){
return payload;
}
}
and this is my controller:
export class AuthController {
constructor(private authService: AuthService) {}
#UseGuards(AuthGuard("jwt"))
#Get()
getPayload() {
//here I need to get the payload that was returned in jwt strategy
}
}
So how can I get the payload in controller that was returned in jwt strategy?
the value returned/resolved by JwtStrategy#validate will be the value of req.user
import { Req } from '#nestjs/common'
// ...
#UseGuards(AuthGuard("jwt"))
#Get()
getPayload(#Req() req: any) {
console.log(req.user)
}
https://docs.nestjs.com/security/authentication#login-route

Passport login not calling the function in strategy file

I am trying to use Passport for SSO. My problem is that when I log in with any of the options everything is fine, except the data saving... I think the functions in the strategy files are not called (the log is not working neither).
For example the Google strategy:
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(private userService: UserService) {
super({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:4200',
scope: ['email', 'profile'],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
try {
console.log(profile);
const user = profile;
this.userService.FindOrCreate(profile);
done(null, user);
} catch (err) {
done(err, null);
}
}
}
Controller:
#Get('google')
#UseGuards(AuthGuard('google'))
async twitterauth(#Req() req) {
return await this.authService.login(req.user);
}
AuthService:
#Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
constructor(
private userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User> {
const user: User = await this.userService.findOne({
where: { email },
});
if (!user) {
return null;
} else {
if (await bcrypt.compare(password, user.password)) {
return user;
} else {
this.logger.error('Password is incorrect.');
return null;
}
}
}
async login(user: any) {
const payload = { email: user.email, role: user.role };
return {
// eslint-disable-next-line #typescript-eslint/camelcase
access_token: this.jwtService.sign(payload),
};
}
}
The other strategies (fb, linkedin, instagram, github) are quite the same and the problem is the same.
The problem, as found in chat, was that the callback that Google was calling to in the OAuth flow, was not a part of the same server, and as such, the NestJS server could not react to the incoming data, hence why the validate was never called.
That callback route needs to point to your NestJS server so that it can handle the saving logic for the database,OR the angular applications needs to re-route the return to it back to the NestJS server. Either way, your validations aren't being called because your Nest server never gets the callback with all the sensitive information from Google
More than likely, it will be better to have the callback pointed at your server so that the data is formatted as Passport is expected.

Resources