I'm trying to secure a nestjs api project using a jwt generated by google oauth. I have the client code working and have verified with jwt.io that the jwt being generated is correct and validates ok with the client secret I have.
I have followed the guide from nestjs for implementing passport and the jwt auth guard, however all I get when I pass a bearer token to an method with the guard is JsonWebTokenError: invalid algorithm
The relevant code snippits:
jwt.strategy.ts
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: true,
secretOrKey: privateKey,
algorithms: ['HS256']
});
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
async validate(payload: any) {
const user = await this.authService.validateUser(+payload.sub);
console.log(user);
return user;
}
}
jwt.auth-guard.ts
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info, context) {
// valid Token: empty log line
// invalid Token: empty log line
console.log(err, 'error');
// valid Token: token object
// invalid Token: false
console.log(user, 'user');
// valid Token: empty log line
// invalid Token: error object from passport
console.log(info, 'info'); <---- ERROR COMES FROM HERE
// valid Token: no log line at all
// invalid Token: no log line at all
console.log(context, 'context');
if (err || !user) {
console.error(`JWT Authentication error: ${err}`);
throw err || new UnauthorizedException();
}
return user;
}
}
auth.module.ts
imports: [
PassportModule.register({ defaultStrategy: 'jwt', session: true }),
JwtModule.register({
secretOrPrivateKey: privateKey,
signOptions: {
expiresIn: 3600,
algorithm: 'HS256'
}
}),
UsersModule
],
providers: [AuthService, JwtStrategy],
exports: [AuthService]
})
export class AuthModule {}
Related
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 }),
};
I'm trying to pass some data from my local backend using nest.JS, the login is successful and the jwt token is shown in the cookies, but the error says:
[Nest] 39 - 02/22/2022, 9:50:59 AM ERROR [ExceptionsHandler] jwt must be provided
nest-admin-backend-1 | JsonWebTokenError: jwt must be provided
nest-admin-backend-1 | at Object.module.exports [as verify] (/app/node_modules/jsonwebtoken/verify.js:53:17)
nest-admin-backend-1 | at /app/node_modules/#nestjs/jwt/dist/jwt.service.js:42:53
nest-admin-backend-1 | at new Promise (<anonymous>)
nest-admin-backend-1 | at JwtService.verifyAsync (/app/node_modules/#nestjs/jwt/dist/jwt.service.js:42:16)
nest-admin-backend-1 | at AuthService.userId (/app/src/auth/auth.service.ts:16:44)
nest-admin-backend-1 | at AuthController.user (/app/src/auth/auth.controller.ts:68:43)
nest-admin-backend-1 | at /app/node_modules/#nestjs/core/router/router-execution-context.js:38:29
nest-admin-backend-1 | at processTicksAndRejections (node:internal/process/task_queues:93:5
as for the code, there is no error anywhere, I'm following the tutorial as it should and it doesn't work.
If I'm following the error message, it says the error on my auth.service & auth.controller file, so here is my file snippets:
Auth.controller
export class AuthController {
constructor(
private userService: UserService,
private jwtService: JwtService,
private authService: AuthService,
) {
}
#Post('register')
async register(#Body() body: RegisterDto) {
if (body.password !== body.password_confirm) {
throw new BadRequestException('Password do not match!');
}
const hashed = await bcrypt.hash(body.password, 12);
{ }
return this.userService.create({
firstName: body.firstName,
lastName: body.lastName,
email: body.email,
password: hashed,
role: { id: 1 }
});
}
#Post('login')
async login(
#Body('email') email: string,
#Body('password') password: string,
#Res({ passthrough: true }) response: Response,
) {
const user = await this.userService.findOne({ email });
if (!user) {
throw new NotFoundException('User not found!');
}
if (!await bcrypt.compare(password, (await user).password)) {
throw new BadRequestException('Invalid password!');
}
// Generate JWT
const jwt = await this.jwtService.signAsync({ id: user.id })
response.cookie('jwt', jwt, { httpOnly: true });
return user;
}
#UseGuards(AuthGuard) // This is a custom guard
// Authenticate user and generate JWT
#Get('user')
async user(#Req() request: Request) {
const id = await this.authService.userId(request);
// Get user from the database
return this.userService.findOne({ id });
}
#UseGuards(AuthGuard) // Check if user is authenticated
#Post('logout')
async logout(#Res({ passthrough: true }) response: Response) {
response.clearCookie('jwt');
return {
message: 'Logged out successfully',
}
}
}
Auth.service
export class AuthService {
constructor(private jwtService: JwtService) {
}
async userId(request: Request): Promise<number> {
const cookie = request['jwt'];
// Get data from the Cookie
const data = await this.jwtService.verifyAsync(cookie);
// Get user from the database
return data['id'];
}
}
I can't access the localhost:8000/api/user from the postman too, even if I'm already logged in. Any idea how to solve it?
Cookies are a protocol between HTTP servers and browsers so Postman and Backends can't just log in and have the cookie header sent.
To allow Applications (mobile, desktop, and server) to be identified by your API server will need to introduce an additional way to send the JWT.
Allow JWT to be sent as an HTTP header in addition to Cookies. Use the Authorization header as a secondary method to send the JWT in.
To achieve this you will need to:
modify your /login to return the JWT as plain text (instead of User)
#Post('login')
async login(
#Body('email') email: string,
#Body('password') password: string,
#Res({ passthrough: true }) response: Response,
) {
const user = await this.userService.findOne({ email });
...
// Generate JWT
const jwt = await this.jwtService.signAsync({ id: user.id })
response.cookie('jwt', jwt, { httpOnly: true }); // <-- for browsers
return jwt; // <--- for applications
}
Now it's the application's responsibility to store and send JWT (using the Authorization header) on subsequences request
Update your AuthGuard to check for JWT in the cookie and Authorization header.
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/
I using nest.js + passport + jwt + graphql in project.
If there is a token, then decoded information,
want to get undefined if don't have token.
Always must have Guards to receive decoded tokens.
Can I selectively generate 401 error?
#Module({
providers: [
JwtStrategy,
],
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: 'hi',
signOptions: { expiresIn: '1d' },
}),
],
})
export class AuthModule {
}
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExcutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: config.jwt.secret,
})
}
validate(payload: JwtPayload): any {
return { user_id: +payload.user_id , username: payload.username }
}
}
#Resolver(() => Test)
export class TestResolver {
#UseGuards(GqlAuthGuard) // I want to get validated users without guards.
#Query(() => Test)
startTest(
#User() user: any, // req.user
) {
console.log(user)
}
}
I don't know if it's possible, but I want this code.
app.use((req, res, next) => {
if (req.headers.token) { // optional
try {
req.user = verify(req.headers.token)
} catch (err) {
req.user = undefined
}
}
next()
})
app.get((req, res, next) => {
if (req.user) {
...
}
})
You can apply global guards like so
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
const reflector = app.get(Reflector);
app.useGlobalGuards(new GqlAuthGuard(reflector));
await app.listen(process.env.PORT);
}
I am using nestjs and having an issue with using guards to authenticate a request.
My JwtStrategy is never execute.
Here is my JwtStrategy :
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(
#Inject('IQueryBusAdapter')
private readonly queryBus: IQueryBusAdapter,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET_KEY,
});
}
validate = async (payload: IJwtPayload) => {
Logger.log('INNNNN');
const query = new GetUserByIdQuery();
query.id = payload.id;
const user = await this.queryBus.execute(query);
if (!(user instanceof User)) {
throw new UnauthorizedException();
}
return user;
};
}
I also try to call directly validate() directly in constructor but nothing change...
My AuthModule :
#Module({
imports: [
BusModule,
JwtModule.register({
secretOrPrivateKey: process.env.JWT_SECRET_KEY,
signOptions: {
expiresIn: process.env.JWT_EXPIRES,
},
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
TypeOrmModule.forFeature([User]),
],
controllers: [RegisterAction, LoginAction],
providers: [
JwtStrategy,
],
})
export class AuthModule {}
My controller
#Get('/:id')
#ApiOperation({ title: 'Get user ressource' })
#UseGuards(AuthGuard('jwt'))
async index(#Param() query: GetUserByIdQuery): Promise<object> {
const user = await this.queryBus.execute(query);
if (!(user instanceof User)) {
throw new NotFoundException();
}
return {
id: user.id,
fullName: user.getFullName(),
email: user.email
};
}
I always received 401 status code.
Thanks for your help.
validate will only be called when you pass a valid jwt token. When the token is signed with a different secret or is expired, validate will never be called. Make sure you have a valid token. You can check your token with the jwt debugger.