pyjwt: The token is not yet valid (iat).

We found an issue in one of our tests in the recent update of yours. Our package works with pyjwt==2.5.0 but it breaks with pyjwt==2.6.0. We will give more details on the issue once we explore it more and will try to provide a minimal example that reproduces the error. However, may I ask you to check if the latests changes could have broken backawards compatibility? We are particularly suspicious of #794. We think this because the error message we get: The token is not yet valid (iat).

We are sorry we could not provide more details yet.

System Information

$ python -m jwt.help
{
  "cryptography": {
    "version": "38.0.1"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.10.6"
  },
  "platform": {
    "release": "5.19.16-76051916-generic",
    "system": "Linux"
  },
  "pyjwt": {
    "version": "2.6.0"
  }
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 4
  • Comments: 21 (6 by maintainers)

Commits related to this issue

Most upvoted comments

I’ve also encountered this issue, particularly in tests where the token is issued within a few milliseconds of when it gets used. The problem is that the iat is assumed to be an integer, while the RFC allows floats. If a token is issued with a float iat and decoded within that second, the code will test against the integer second, which is less than the float iat. This results in ImmatureSignatureError being raised.

Here is an example that used to succeed, but now raises an error:

jwt.decode(jwt.encode({'iat': time.time()}, 'secret', algorithm='HS256'), 'secret', algorithms=['HS256'])

I suggest either allowing full float calculations or casting the input iat to an int.

Y’all this was litigated previously at https://github.com/jpadilla/pyjwt/issues/190. The JWT spec does NOT say to reject tokens with iat (“issued at”) in the future, so this behavior goes beyond the spec and is inconsistent with many other JWT libraries.

4.1.6. “iat” (Issued At) Claim

The “iat” (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.

If token issuers want clients to specify that a token should not be accepted before a certain timestamp (which puts additional constraints upon clients by implying that clients’ clocks should keep relatively in sync with a central clock source and/or need to check it with leeway) then the issuer is supposed to set nbf (“not before”):

4.1.5. “nbf” (Not Before) Claim

The “nbf” (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the “nbf” claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the “nbf” claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.

I am currently getting bit by this issue a lot in a large enterprise microservices environment (where token issuer, token user, and token-accepting server which validates offline using RSA are three different machines) where clock drift is coming into play. Myself and 10+ other people have now wasted multiple days of person-time trying to figure out why tokens were being rejected, talking about how best to address it, and managing the workloads & deliverables of the people investigating & talking about this.

Could this issue be re-opened and could the proper fix be to set the default value of verify_iat to False and publish a 3.0 since that’s a breaking change?

Clients who understand the risks and want to engage in this extra-spec behavior should opt in by setting verify_iat to True, and the need to do this should be announced in the changelog for this new major version. (Or maybe there could be a global variable in pyjwt to control the verify_iat default?)

Hi. Had the same issue, when updating from 2.4.0 to 2.6.0… drove me crazy this bug.

Solved it by simply deactivate the check with: options={“verify_iat”:False} in decode:

self.decoded_token = jwt.decode( token_str, ActivationToken.pub_key, algorithms=["RS256"], options={"verify_iat":False} ) But I have to admit, I have wrote a custom check for the iat already before that update with an higher tolerance value (I think this is what you have labeled as leeway).

It’s yes and no, since most JWT libraries in other languages also support the int type this could cause an issue. I agree with the fact that float has a bit more precision than int but to tackle this synchronization issue the recommended way is leeway time. So adding the minimum leeway time will help them without changing the custom logic they have now.

An example could be, jwt.decode(jwt.encode({'iat': int(time.time())+leeway}, 'secret', algorithm='HS256'), 'secret', algorithms=['HS256'])

With this, PyJWT will be in sync with other frameworks/libraries in other languages as well.

Lately, I’ve been encountering this error The token is not yet valid (iat) (as of March 2024). Interestingly, the solution was simply to resynchronize my computer clock on Windows 11 23H2 (OS build 22631.3296) working on WSL2. Has anyone else encountered this issue where the clock gets out of sync, as I have experienced it multiple times this week! My computer is relatively new, so I doubt the MB battery is causing the problem.

Just for fairness, I did some due diligence on the other libraries. Every other library I’ve found that does this has its own respective issue complaining about it. 😉

Libraries I found that do not check that iat is >= now:

Libraries that do (and their respective bug complaints):

Maybe this needs to be clarified in the spec since there’s a pretty divided polity…

what if instead of: https://github.com/jpadilla/pyjwt/blob/0cfc0978a6c9dc01d89043d203b7f397e78822c2/jwt/api_jwt.py#L190

we just did

now = datetime.now(tz=timezone.utc).timestamp()

Wouldn’t that mostly address the issues seen? Anything else would be addressed by specifying a leeway when decoding. unsure if i’m missing anything else.

Different servers could have different time settings, especially when they are not connected to the internet, like a private network. Not every private network has a clock synchronization mechanism.

I’m going to be submitting an errata request for RFC 7519 about this. I’ll be making my draft of that & centralizing the Github-side discussion here:

Discussion: JWT tokens containing iat values in the future should not be rejected

@jpadilla your solution should work,

I also got a few tests failing when upgrading pyjwt :

/usr/local/lib/python3.9/site-packages/jwt/api_jwt.py:193: in _validate_claims
    self._validate_iat(payload, now, leeway)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <jwt.api_jwt.PyJWT object at 0xffff9257b910>
payload = {'access': True, 'event_id': 10, 'exp': 1667564373.7657802, 'iat': 1667564058.7661693, ...}, now = 1667564058
leeway = 0

    def _validate_iat(self, payload, now, leeway):
        iat = payload["iat"]
        try:
            int(iat)
        except ValueError:
            raise InvalidIssuedAtError("Issued At claim (iat) must be an integer.")
        if iat > (now + leeway):
>           raise ImmatureSignatureError("The token is not yet valid (iat)")
E           jwt.exceptions.ImmatureSignatureError: The token is not yet valid (iat)

/usr/local/lib/python3.9/site-packages/jwt/api_jwt.py:219: ImmatureSignatureError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /usr/local/lib/python3.9/site-packages/jwt/api_jwt.py(219)_validate_iat()
-> raise ImmatureSignatureError("The token is not yet valid (iat)")
(Pdb) iat
1667564058.7661693
(Pdb) now
1667564058
(Pdb)

the value of «now» seems not to include milliseconds, because

per timegm source code, it’s only seconds :

def timegm(tuple):
    """Unrelated but handy function to calculate Unix timestamp from GMT."""
    year, month, day, hour, minute, second = tuple[:6]
    days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
    hours = days*24 + hour
    minutes = hours*60 + minute
    seconds = minutes*60 + second
    return seconds

we could bypass this with a leeway of 1 second, but that’s a lot of leeway