Search code examples
javascriptjwtnestjs

NestJS JwtService verifyToken() reads wrong 'nbf' JWT date


I'm using a JwtService in a NestJS app. Jwt is initialized like so:

import { JwtModule } from '@nestjs/jwt';
// ...
JwtModule.register({
    secret: 'mysecret', // TODO: Change to production injected value.
    signOptions: { expiresIn: '1h' },
}),

I created a Jwt and signed it like so:

import { JwtService, JwtSignOptions } from '@nestjs/jwt';

// ...

constructor(
    private readonly jwtService: JwtService,
) {}

// ...

const userInfo = {
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName
};
const accessTokenPayload = {
    iss: 'Auth Server 3.2',     // Issuer
    sub: user.id.toString(),    // Subject (user ID)
    aud: ['myClient'],          // Audience (recipient(s))
    exp: (new Date()).getTime() + 24 * 60 * 60 * 1000,  // Expiration time (Unix timestamp)
    nbf: (new Date()).getTime() - 60 * 60 * 1000,
    iat: new Date().getTime(),              // Issued at (Unix timestamp)
    jti: randomBytes(32).toString('hex'),   // JWT ID (unique identifier)
    data: { userInfo }
}
const accessToken = this.jwtService.signAsync(accessTokenPayload);

This results in the following token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBdXRoIFNlcnZlciAzLjIiLCJzdWIiOiIxIiwiYXVkIjpbIm15Q2xpZW50Il0sImV4cCI6MTY4MzE4Nzc4OTIzNSwibmJmIjoxNjgzMDk3Nzg5MjM1LCJpYXQiOjE2ODMxMDEzODkyMzUsImp0aSI6ImM5ZTIwY2E5ODAwNzEzNGUyNWFmNjk3MzZmZTIyOGM3MTQyNzBhMWQ1NWY5OGFjOWVjZTU2NmQxOTAyYWJiYzUiLCJkYXRhIjp7InVzZXJJbmZvIjp7ImVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJmaXJzdE5hbWUiOiJKb2huIiwibGFzdE5hbWUiOiJTbWl0aCJ9fX0.WSJsnzsZNrMPOQLuPbDoeigFeB6IRJFmXo2qUSFCi3k

which has the following payload:

{
  "iss": "Auth Server 3.2",
  "sub": "1",
  "aud": [
    "myClient"
  ],
  "exp": 1683187789235,
  "nbf": 1683097789235, // valid nbf value = 2023-05-03T07:09:49.235Z
  "iat": 1683101389235,
  "jti": "c9e20ca98007134e25af69736fe228c714270a1d55f98ac9ece566d1902abbc5",
  "data": {
    "userInfo": {
      "email": "[email protected]",
      "firstName": "John",
      "lastName": "Smith"
    }
  }
}

The Problem: (Please read carefully because this is not a duplicate question)

When verifying the JWT by this code:

try {
    await this.jwtService.verifyAsync(accessToken);
} catch(e) {
    console.log('Error', e)
}

I'm getting the following error:

Error NotBeforeError {
        name: 'NotBeforeError',
        message: 'jwt not active',
        date: +055303-05-11T16:49:49.000Z  // --> note the invalid nbf value
}

Note the odd future date in year 55303: date: +055303-05-11T16:49:49.000Z for the nbf field. = 'Fri, 11 May 55303 16:49:49 GMT'

Any ideas what I'm doing wrong?

PS:

  1. Commenting the nbf field solves this, but I lose the ability to validate that the token is not used before a certain date.
  2. Choosing a different value for nbf (ex. nbf: 1683097789235 that is equal to 2023-05-03T07:09:49.235Z) gives the same error.

Solution

  • The problem lies in the date input parameters given for the signing method:

    exp: (new Date()).getTime() + 24 * 60 * 60 * 1000,  
    nbf: (new Date()).getTime() - 60 * 60 * 1000,
    iat: new Date().getTime(),
    

    Even though these values (from the encoded JWT) translate to valid dates when creating a new javascript dates from them:

    "exp": 1683187789235, // new Date(1683187789235) = 2023-05-04T08:09:49.235Z = valid date
    "nbf": 1683097789235, // new Date(1683097789235) = 2023-05-03T07:09:49.235Z = valid date
    "iat": 1683101389235, // new Date(1683101389235) = 2023-05-03T08:09:49.235Z = valid date
    

    The values are not valid JWT dates, because the JWT standard requires UNIX time in seconds, while in JS the UNIX timestamp is based on milliseconds.

    Therefore, the answer to the question is to convert the Dates provided to the jwt siging function (signAsync) from milliseconds to seconds, like so:

    const accessTokenPayload = {
        iss: 'Auth Server 3.2',     // Issuer
        sub: user.id.toString(),    // Subject (user ID)
        aud: ['myClient'],          // Audience (recipient(s))
        exp: Math.floor(((new Date()).getTime() + 24 * 60 * 60 * 1000) / 1000),  // Expiration time (Unix timestamp)
        nbf: Math.floor(((new Date()).getTime() - 60 * 60 * 1000) / 1000),
        iat: Math.floor(((new Date()).getTime()) / 1000),              // Issued at (Unix timestamp)
        jti: randomBytes(32).toString('hex'),   // JWT ID (unique identifier)
        data: { userInfo }
    }
    

    Note: To check the actual values of the a timestamp in a JWT, don't use new Date(valueFromJwt), but rather use https://jwt.io/ and hover over the time fileds to see how the jwt interprets the date: enter image description here