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
- 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
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
- 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
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
- Access token expired
- Access token invalid - signature likely incorrect
- either the tool proxy or developer key were not found
- Invalid access token field/s: #
- invalid aud
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)