pyjwt: Why validate that 'iat' is not in the future?

In https://github.com/jpadilla/pyjwt/blob/c5ee34e86bc42bef60ef6e701df569c2c86a5d5d/jwt/api_jwt.py#L129:

if iat > (now + leeway):
    raise InvalidIssuedAtError('Issued At claim (iat) cannot be in'
                               ' the future.')

I just debugged an issue in prod where jwt.decode() failed because of this. Mostly because the other party’s jwt lib added ‘iat’ a few seconds or minutes ahead of our clock time (‘clock skew’ as mentioned in JWT specs).

I can’t find any place in the specs that says that a JWT should be invalid if ‘iat’ is in the future. It seems like it’s just there to be informative. I can use ‘nbf’ if I want to specify a “time before which the token MUST NOT be accepted for processing”

I consulted

So either

  1. I’m wrong and there is a JWT spec that says this is important to check. I want to know this, because if it’s out there, I shouldn’t just catch these errors from PyJWT and pass. Regardless of whether @jpadilla wants to remove that raise in his lib.
  2. PyJWT is checking that unnecessarily, and we should remove it to be more compliant

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 1
  • Comments: 16 (4 by maintainers)

Commits related to this issue

Most upvoted comments

RFC 7519 says:

The “iat” (issued at) claim identifies the time at which the JWT was issued.

In all circumstances except clock skew, it doesn’t seem like it makes any sense for a JWT to be valid if, by its own claim, it hasn’t been created yet. That’s why we included this validation. JWT doesn’t say you have to have this validation to be compliant, but it seems logical and enforces integrity in the data.

I’m against removing this validation, especially when you can already easily disable it by using jwt.decode(token, secret, options={'verify_iat': False}) or account for the clock skew by using the leeway parameter: jwt.decode(token, secret, leeway=10)

Hi, here’s a real word case of this issue causing problems.

Our infrastructure involves generating tokens on a server in Amazon’s cloud and checking them on another one in Google’s cloud using jwt.decode().

At midnight between 2016 December 31 and 2017 January 1, a leap second was introduced into UTC. Both clouds ‘smeared’ this second linearly over a period of time centered around that midnight: https://aws.amazon.com/blogs/aws/look-before-you-leap-december-31-2016-leap-second-on-aws/ https://developers.google.com/time/smear

However, Amazon used 24 hour period and Google used 20 hour. This led to Amazon’s clock being ahead of Google’s on December 31 between 12:00:00 and 23:59:59, and our service was unavailable during most of that period due to jwt.decode() failing.

So is the recommended fix to just add the options kwarg on all decode calls? Is there some package config I can tweak or something I can monkeypatch to disable it globally?

Still seems annoying to be bitten by enforcing something not mentioned in the RFC. All it says is that (emph. added)

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.

A negative age is startling I suppose, but nothing else seems to say that’s forbidden.

Thanks petevg! Glad you enjoy it! 😃

A couple of things in response:

How can it be unreasonable to assume two computers have a similar definition of when “now” is? We make these sort of assumptions all the time in all sorts of systems (public-key certificates used for HTTPS, EXP claims on JWT, TOTP-based two-factor authentication, etc.). I think it is completely reasonable, in particular when working with security, to assume two computers have a similar understanding of when “now” is. I do believe it IS unreasonable however to assume they have the EXACT same definition of now. That’s why we include the ability to add the leeway parameter as almost everything on the Internet that relies on time does to compensate for the fact that exact time synchronization is hard.

Setting verify_iat to default to False is an option but we’d probably want to bump the version number and make it clear if we make that sort of change (because some people may be relying on that behavior). In the mean time, setting options={'verify_iat': False} seems like its still a reasonable workaround.

If there’s a way we can make this behavior clearer in the documentation, I’m 100% sure we’d be glad to do that. Any ideas?

Thanks again for participating in the discussion 👍

Hi there. Since the iat check introduces a situation where the a default install of PyJWT will break in typical production circumstances (or, if you’re lucky, in testing, which is what happened to me), I’d like to second the request to set verify_iat to False by default. I don’t think that it’s reasonable to expect any two servers to have a shared definition of “now”, or to make strict checks based on time between two machines.

Regardless, thank you for all the work putting this library together – it has been really helpful and useful. 😃

Thanks for the PR! Just a reminder, instead of having to fork and package your own version of PyJWT, which seems a bit like overkill, you can totally bypass the check (exactly what your PR does), if you simply pass options={'verify_iat': False} to your jwt.decode() call with the existing code.

I don’t like the idea of removing this check completely. Plenty of libraries have reasonable functionality that is not necessarily part of any particular spec. The strongest argument would be that we should set verify_iat to False by default and leave this as a helpful feature that developers can turn on if they want the additional functionality. That’s a PR I could get behind!

Update for anybody finding this discussion later:

  • iat <= now validation was removed in 1.5.0 via #252
  • iat <= now validation was re-added in 2.6.0 via #794 (current state)

Two other related issues:

  • #814 was deeply related to this issue, but in that particular user’s case ended up being caused by a rounding error instead
  • #821 was marked as “resolving” #814, but only removed the rounding error (it did not remove iat <= now validation)

The issue is more that unless you explicitly disable the check every time or add in leeway for clock skew, this can cause random errors. The RFC talks about minutes of skew, which seems crazy, but even milliseconds of skew can cause a validation error:

  1. Say you have two servers, an Issuer perfectly at UTC and Validator at time = Issuer - 10 milliseconds.
  2. Time is 20000.005 (seconds past epoch)
  3. Issuer creates a token with an 'iat': 20000
  4. Validator reads token, time for it is 19999.996. It rejects the token.

Little demo:

successes = fails = 0
skew = datetime.timedelta(microseconds=10000)
for n in range(10000):
    token = jwt.encode({'iat': datetime.datetime.utcnow() + skew}, 'magic')
    try:
        jwt.decode(token, 'magic')
    except jwt.exceptions.InvalidTokenError:
        fails += 1
    else:
        successes += 1
print(successes, fails) # => 9851 149

@mark-adams: Thanks for the quick response!

I think that the lightest weight change might simply to be to adjust the leeway to be something other than zero by default, and add it as an explicit param to PyJWT.decode, so that it’s easy to find it with introspection tools or in the docstring for that function. As it is, I think that it’s going to be pretty common for people to trip themselves up with the error if they don’t know the external docs by heart.

When choosing between passing a value to leeway or disabling the check, I chose to disable the check. That might be because I’m coming more from a distributed database monkey’s perspective, where I use timestamps to keep a record of things and help servers do things internally, but try to use other methods for syncing state across servers. I may be missing the security implications in skipping the check.

In any case, I think that I’d be happy with either solution – just as long as the solution isn’t to assume that all server clocks are perfectly in sync by default. It definitely makes sense to include it in a later version so that existing code doesn’t break 😃