{"record":{"id":"5736158899d3d029","repo":"instructure/canvas-lms","slug":"iat-must-be-in-the-past","errorCode":null,"errorMessage":"iat must be in the past","messagePattern":"iat must be in the past","errorType":"validation","errorClass":"InvalidTokenError","httpStatus":401,"severity":"error","filePath":"lib/lti/oauth2/access_token.rb","lineNumber":55,"sourceCode":"        raise InvalidTokenError, \"token has expired\", e.backtrace\n      rescue => e\n        raise InvalidTokenError, e\n      end\n\n      def initialize(aud:, sub:, jwt: nil, reg_key: nil, shard_id: nil)\n        @_jwt = jwt if jwt\n        @reg_key = reg_key || (jwt && decoded_jwt[\"reg_key\"])\n        @aud = aud\n        @sub = sub\n        @shard_id = shard_id\n      end\n\n      def validate!\n        decoded_jwt = Canvas::Security.decode_jwt(jwt)\n        check_required_assertions(decoded_jwt.keys)\n        raise InvalidTokenError, \"invalid iss\" if decoded_jwt[\"iss\"] != ISS\n        raise InvalidTokenError, \"invalid aud\" unless [*decoded_jwt[:aud]].include?(aud)\n        raise InvalidTokenError, \"iat must be in the past\" unless Time.zone.at(decoded_jwt[\"iat\"]) < Time.zone.now\n\n        true\n      rescue InvalidTokenError\n        raise\n      rescue Canvas::Security::TokenExpired => e\n        raise InvalidTokenError, \"token has expired\", e.backtrace\n      rescue => e\n        raise InvalidTokenError, e\n      end\n\n      def to_s\n        jwt\n      end\n\n      private\n\n      def decoded_jwt\n        @_decoded_jwt ||= Canvas::Security.decode_jwt(jwt)","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/lti/oauth2/access_token.rb#L37-L73","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Synchronize clocks (NTP) on the issuing and validating servers","Ensure iat is emitted as epoch seconds in the past at token creation","Regenerate the token after fixing the clock and retry"],"exampleFix":"// before\npayload.iat = Date.now(); // milliseconds, and possibly ahead\n// after\npayload.iat = Math.floor(Date.now() / 1000); // epoch seconds, now","handlingStrategy":"validation","validationCode":"iat = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))['iat']\nraise 'iat not in past' unless iat && iat < Time.now.to_i","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Emit iat as epoch seconds (not milliseconds) taken from a synchronized clock","Run NTP on all issuing and validating hosts","Clamp iat slightly into the past (a few seconds) to tolerate skew"],"tags":["lti","oauth2","jwt","clock-skew"],"backgroundTag":"invalid-date-format","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}