I am currently facing an issue with passing a JWT from my client application, which is built using nextJS with nextAuth, to my backend nestJS application that utilizes graphQL. In order to implement an auth guard in my nestJS backend application, I'm attempting to extract the JWT using a custom function in my jwt.strategy.ts
However, despite having a valid signed token, the JwtStrategy is refusing to accept it. To validate the validity of the JWT, I added some console output for the token. Strangely, the validate()
function is never triggered. It's puzzling because the token can be successfully verified using jwt.verify
:
This is the decoded output from the jwt.verify()
:
JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6MTIzLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaXNBZG1pbiI6dHJ1ZX0sImlhdCI6MTYwOTY3NTc4Nn0.LQy4QSesxJR91PyGGb_0mGZjpw9hlC4q7elIDs2CkLo
Secret: uGEFpuMDDdDQA3vCtZXPKgBYAriWWGrk
Decoded: {
user: { userId: 123, username: 'username', isAdmin: true },
iat: 1609675786
}
I am unable to pinpoint the missing piece of the puzzle and I'm at a loss as to how to debug this further, especially since there is no output in my jwt.strategy.ts file and the validate-function remains untouched.
jwt.strategy.ts
import jwt from 'jsonwebtoken'
// import { JwtService } from '@nestjs/jwt'
import { Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable } from '@nestjs/common'
import cookie from 'cookie'
import { getConfig } from '@myapp/config'
const { secret } = getConfig()
const parseCookie = (cookies) => cookie.parse(cookies || '')
const cookieExtractor = async (req) => {
let token = null
if (req?.headers?.cookie) {
token = parseCookie(req.headers.cookie)['next-auth.session-token']
}
// output as shown above
console.log('JWT:', token)
console.log('Secret:', secret)
const decoded = await jwt.verify(token, secret, { algorithms: ['HS256'] })
console.log('Decoded: ', decoded)
return token
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: true,
secretOrKey: secret
})
}
async validate(payload: any) {
console.log('payload:', payload) // is never called
return { userId: payload.sub, username: payload.username }
}
}
jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
import { GqlExecutionContext } from '@nestjs/graphql'
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
getRequest(context: GqlExecutionContext) {
const ctx = GqlExecutionContext.create(context)
return ctx.getContext().req
}
}
The guard is implemented in this resolver:
editor.resolver.ts
import { Query, Resolver } from '@nestjs/graphql'
import { UseGuards } from '@nestjs/common'
import { GqlAuthGuard } from '../auth/jwt-auth.guard'
@Resolver('Editor')
export class EditorResolvers {
constructor(private readonly editorService: EditorService) {}
@UseGuards(GqlAuthGuard)
@Query(() => [File])
async getFiles() {
return this.editorService.getFiles()
}
}
auth.module.ts
import { Module } from '@nestjs/common'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { PassportModule } from '@nestjs/passport'
import { LocalStrategy } from './local.strategy'
import { JwtStrategy } from './jwt.strategy'
import { UsersModule } from '../users/users.module'
import { JwtModule } from '@nestjs/jwt'
import { getConfig } from '@myApp/config'
const { secret } = getConfig()
@Module({
imports: [
UsersModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret,
verifyOptions: { algorithms: ['HS256'] },
signOptions: { expiresIn: '1d' }
})
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService]
})
export class AuthModule {}
The token is generated on the server side (nextJS api page) using:
const encode = async ({ secret, token }) => jwt.sign(token, secret, { algorithm: 'HS256' })