instructure/canvas-lms · error · InvalidTokenError

iat must be in the past

Error message

iat must be in the past

What it means

Lti::OAuth2::AccessToken#validate! requires the iat (issued-at) claim to be strictly in the past relative to Time.zone.now. A token whose iat is in the future (or unparseable to a time) is rejected with InvalidTokenError 'iat must be in the past' to prevent tokens post-dating validation.

Solutions

  1. Synchronize clocks (NTP) on the issuing and validating servers
  2. Ensure iat is emitted as epoch seconds in the past at token creation
  3. Regenerate the token after fixing the clock and retry

Example fix

// before
payload.iat = Date.now(); // milliseconds, and possibly ahead
// after
payload.iat = Math.floor(Date.now() / 1000); // epoch seconds, now
Defensive patterns

Strategy: validation

Validate before calling

iat = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))['iat']
raise 'iat not in past' unless iat && iat < Time.now.to_i

Prevention

When it happens

Trigger: Calling validate! on a JWT whose iat claim is greater than or equal to the current server time, commonly due to clock skew between the issuing and validating machines.

Common situations: Clock drift between tool server and Canvas (unsigned/no-NTP servers), forging tokens with iat set from a misconfigured clock, or time-zone bugs producing future iat values in milliseconds instead of epoch seconds.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/5736158899d3d029. Report an issue: GitHub.

Appendix: source

Thrown at lib/lti/oauth2/access_token.rb:55

        raise InvalidTokenError, "token has expired", e.backtrace
      rescue => e
        raise InvalidTokenError, e
      end

      def initialize(aud:, sub:, jwt: nil, reg_key: nil, shard_id: nil)
        @_jwt = jwt if jwt
        @reg_key = reg_key || (jwt && decoded_jwt["reg_key"])
        @aud = aud
        @sub = sub
        @shard_id = shard_id
      end

      def validate!
        decoded_jwt = Canvas::Security.decode_jwt(jwt)
        check_required_assertions(decoded_jwt.keys)
        raise InvalidTokenError, "invalid iss" if decoded_jwt["iss"] != ISS
        raise InvalidTokenError, "invalid aud" unless [*decoded_jwt[:aud]].include?(aud)
        raise InvalidTokenError, "iat must be in the past" unless Time.zone.at(decoded_jwt["iat"]) < Time.zone.now

        true
      rescue InvalidTokenError
        raise
      rescue Canvas::Security::TokenExpired => e
        raise InvalidTokenError, "token has expired", e.backtrace
      rescue => e
        raise InvalidTokenError, e
      end

      def to_s
        jwt
      end

      private

      def decoded_jwt
        @_decoded_jwt ||= Canvas::Security.decode_jwt(jwt)

View on GitHub (pinned to 1c9f0bb801)