Nestjs Passport-jwt better Unauthorized strategy - nestjs

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.

Related

Nestjs returns 401 (Unauthorized) even with valid user ft. passport-local

Hi awesome developers,
I'm trying to implement Authentication using passport-local and Nestjs with reference to https://progressivecoder.com/how-to-implement-nestjs-passport-authentication-using-local-strategy/.
I have implemented exactly same but Nestjs always returns 401 Unauthorized even with valid user. I can't seem to find what I am missing.
Code Structure
Authentication Module
User Module
Here's the code:
Authentication Module:
authentication.module.ts
import { Module } from '#nestjs/common';
import { PassportModule } from '#nestjs/passport';
import { UserModule } from 'src/user/user.module';
import { AuthenticationController } from './controllers/authentication.controller';
import { AuthenticationService } from './services/authentication.service';
import { LocalStrategy } from './strategies/local.strategy';
#Module({
imports:[UserModule, PassportModule],
controllers: [AuthenticationController],
providers: [AuthenticationService, LocalStrategy]
})
export class AuthenticationModule {}
authentication.controller.ts
import { Controller, Post, Request, UseGuards } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
import { UserService } from 'src/user/services/user.service';
#Controller('authentication')
export class AuthenticationController {
constructor(private userService: UserService){}
#UseGuards(AuthGuard('local'))
#Post('signin')
async signin(#Request() req){
return req.user;
}
}
authentication.service.ts
import { Injectable } from '#nestjs/common';
import { UserService } from 'src/user/services/user.service';
#Injectable()
export class AuthenticationService {
constructor(private userService: UserService) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.userService.readUserByEmail(email);
if (user && user.password === password) {
const { password, ...result } = user;
return result;
}
return null;
}
}
local.strategy.ts
import { Injectable, UnauthorizedException } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import { Strategy } from "passport-local";
import { AuthenticationService } from "../services/authentication.service";
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy){
constructor(private authenticationService: AuthenticationService){
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authenticationService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
User Module:
user.module.ts
import { Module } from '#nestjs/common';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
#Module({
controllers: [UserController],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
user.controller.ts
Skipping user controller as it is irrelevent
user.service.ts
import { Injectable } from '#nestjs/common';
import { prisma } from 'src/main';
import { CreateUserDTO } from '../dto/create-user.dto';
import { UpdateUserDTO } from '../dto/update-user.dto';
#Injectable()
export class UserService {
private readonly users = [
{
id: "1",
name: "Ajitesh",
email: "ajitesh.k-s#lloydsbanking.com",
password: "secret"
}
]
//....other methods
async readUserByEmail(email: string){
return this.users.find(user => user.email === email);
}
}
Request:
{
"email": "ajitesh#example.com",
"password": "secret"
}
Thanks in advance.
passport-local expects req.body to be populated with username and password fields. If you plan to use something else for the username, like email, then you need to tell passport about that in your strategy using the usernameField option in super
import { Injectable, UnauthorizedException } from "#nestjs/common";
import { PassportStrategy } from "#nestjs/passport";
import { Strategy } from "passport-local";
import { AuthenticationService } from "../services/authentication.service";
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy){
constructor(private authenticationService: AuthenticationService){
super({
usernameField: 'email',
});
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authenticationService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

NestJS passport-jwt always throws Unauthorized

I encountered to the issue that whenever I sign up through the postman I receive the token. However, other route is private and requires token as Authorization Bearer, but whenever I put the token I receive "Unauthorized". The validate from strategy never executes, as I understand because the token for some reasons is invalid. Important to mention, that I do not receive any errors.
jwt.strategy.ts:
import { Injectable } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get('JWT_SECRET'),
});
}
async validate(payload: any) {
console.log({ payload: payload });
return true;
}
}
auth.module.ts
import { Module } from '#nestjs/common';
import { JwtModule } from '#nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategy';
#Module({
imports: [JwtModule.register({})],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
auth.service.ts where I receive token:
public async signin(body: AuthDto) {
// find the user by email
const user = await this.prisma['User'].findUnique({
where: {
email: body.email,
},
});
// if user does not exists throw exception
if (!user) {
throw new ForbiddenException('Invalid email address or password');
}
// compare passwords
const passwordMatches = await argon.verify(user.hash, body.password);
// if password is inoccrect throw exception
if (!passwordMatches) {
throw new ForbiddenException('Invalid email address or password');
}
return this.signToken(user.id, user.email);
}
auth.service.ts where I create the token:
private async signToken(userId: number, email: string): Promise<{ access_token: string }> {
const payLoad = {
sub: userId,
email,
};
const token = await this.jwt.sign(payLoad, {
expiresIn: '10m',
secret: this.config.get('JWT_SECRET'),
});
return { access_token: token };
}
}
user.controller.ts where the private route is
import { Controller, Get, UseGuards } from '#nestjs/common';
import { JwtGuard } from 'src/auth/guard';
#Controller('users')
export class UserController {
#UseGuards(JwtGuard)
#Get('me')
getMe() {
return 'Hello JWT';
}
}
jwt.guard.ts
import { AuthGuard } from '#nestjs/passport';
export class JwtGuard extends AuthGuard('jwt') {
constructor() {
super();
}
}
The code works fine, without any issues. However, the issues was with the postman. For some reasons the Authorization tab were bugged, tried to sent through headers written by hand and that worked without any problems

Passport-JWT throwing secretOrKey Returns Uauthorized (Firebase Auth Guard)

I am using firebase to authenticate my users. But sometimes when I request a router (using a bearer token) Then I have to change my secretOrKey. I have no idea what happens to my Auth Guard.
I follow this article to create this Auth guarde
My PassportStrategy: -
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '#nestjs/config';
#Injectable()
export class AuthStrategy extends PassportStrategy(Strategy) {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('JWT_KEY').replace(/\\n/g, '\n'),
algorithms: ['RS256'],
});
}
async validate(payload) {
return {
user_id: payload.user_id,
email: payload.email,
role: payload.role,
};
}
}
JWT_KEY:-
I get my JWT_KEY for secretOrKey using this link.

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 authentication with JWT strategy - add validation option of "ignoreNotBefore"

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/

Resources