{"record":{"id":"c0d3d91ded88f686","repo":"instructure/canvas-lms","slug":"token-has-expired","errorCode":null,"errorMessage":"token has expired","messagePattern":"token has expired","errorType":"exception","errorClass":"InvalidTokenError","httpStatus":401,"severity":"error","filePath":"lib/lti/oauth2/access_token.rb","lineNumber":37,"sourceCode":"\nmodule Lti\n  module OAuth2\n    class AccessToken\n      private_class_method :new\n\n      ISS = \"Canvas\"\n\n      attr_reader :aud, :sub, :reg_key, :shard_id\n\n      def self.create_jwt(aud:, sub:, reg_key: nil)\n        new(aud:, sub:, reg_key:, shard_id: Shard.current.id)\n      end\n\n      def self.from_jwt(aud:, jwt:)\n        decoded_jwt = Canvas::Security.decode_jwt(jwt)\n        new(aud:, sub: decoded_jwt[:sub], jwt:, shard_id: decoded_jwt[:shard_id])\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 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","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/lti/oauth2/access_token.rb#L19-L55","documentation":"Lti::OAuth2::AccessToken.from_jwt decodes a Canvas-issued LTI OAuth2 JWT with Canvas::Security.decode_jwt, which raises Canvas::Security::TokenExpired if the exp claim has passed. from_jwt rescues that and raises InvalidTokenError with the message 'token has expired'.","triggerScenarios":"Calling Lti::OAuth2::AccessToken.from_jwt with a JWT whose exp is in the past - typically a service-JWT produced by AccessToken.create_jwt and consumed later than 1 hour after issuance.","commonSituations":"Message/auth tokens from LTI launches stored and replayed later, queued jobs processing stale launch data, clock skew making tokens appear expired, or test fixtures with hardcoded past exp values.","solutions":["Mint a fresh token via Lti::OAuth2::AccessToken.create_jwt (or from_jwt on a new JWT) and retry","Process launch/service payloads promptly instead of storing tokens for later use","If expiry seems premature, synchronize clocks and verify the exp value embedded at issuance"],"exampleFix":"# before\ntoken = stored_launch_token\nLti::OAuth2::AccessToken.from_jwt(aud: aud, jwt: token).validate!\n# after\nraise Lti::OAuth2::AccessToken::InvalidTokenError, 'stored token too old' if token_expired?(stored_launch_token)\ntoken = Lti::OAuth2::AccessToken.create_jwt(aud: aud, sub: sub).to_s","handlingStrategy":"try-catch","validationCode":"exp = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))['exp']\nraise 'token expired' if exp && exp <= Time.now.to_i","typeGuard":null,"tryCatchPattern":"begin\n  token = Lti::OAuth2::AccessToken.from_jwt(aud: aud, jwt: jwt)\nrescue Lti::OAuth2::AccessToken::InvalidTokenError => e\n  Rails.logger.warn(\"invalid LTI token: #{e.message}\")\n  jwt = Lti::OAuth2::AccessToken.create_jwt(aud: aud, sub: sub).to_s\n  retry\nend","preventionTips":["Consume launch/service tokens promptly; don't store them beyond exp","Use a short cache TTL and re-mint via create_jwt when stale","Synchronize clocks between issuing and validating hosts","Validate fixtures in tests have future exp values"],"tags":["lti","oauth2","jwt","expired-token"],"backgroundTag":"jwt-token-expired","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"}