2
votes

I'm passing a JWT from my client application (nextJS with nextAuth) via credentials header to my backend nestJS application (which is using graphQL). In my nestJS backend application I'm trying to implement an auth guard, so I extract the JWT with a custom function in my jwt.strategy.ts

But the JwtStrategy is not accepting my valid signed token. To prove that the JWT is valid, I put some console output for the token. But the validate() function is never called. I do not understand why, as the token can be validated with jwt.verify:

This is my output - it gets decoded by the jwt.verify():

JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6MTIzLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwiaXNBZG1pbiI6dHJ1ZX0sImlhdCI6MTYwOTY3NTc4Nn0.LQy4QSesxJR91PyGGb_0mGZjpw9hlC4q7elIDs2CkLo
Secret: uGEFpuMDDdDQA3vCtZXPKgBYAriWWGrk
Decoded: {
  user: { userId: 123, username: 'username', isAdmin: true },
  iat: 1609675786
}

I don't see, what I am missing and I even do not see how to debug it as there is no output in my jwt.strategy.ts and the validate-function is not called at all.

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 used 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 created on server side (nextJS api page) with:

const encode = async ({ secret, token }) => jwt.sign(token, secret, { algorithm: 'HS256' })
1

1 Answers

2
votes

I see 2 differences from nestJS docs examples from your jwt.strategy.ts file that you can change and give it a try..

https://docs.nestjs.com/security/authentication#implementing-passport-jwt

  1. sync extractor and not async

By default passport-jwt extractor we can see that is an sync and not async, so you can try change your extractor and remove the async, or to add await when calling it.

https://github.com/mikenicholson/passport-jwt/blob/master/lib/extract_jwt.js , look for fromAuthHeaderAsBearerToken function.

so or change your

const cookieExtractor = async (req) => {

to

const cookieExtractor = (req) => {

OR - add await when you call it

jwtFromRequest: await cookieExtractor(),
  1. call the extractor and not just pass it

by the docs example in JwtStrategy constructor they calling the extractor and not passing it like you do

jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

so try to call it in your JwtStrategy constructor

jwtFromRequest: cookieExtractor(),    // (again - take care of sync / async)