{"record":{"id":"260b30996e126c45","repo":"instructure/canvas-lms","slug":"invalid-token","errorCode":null,"errorMessage":"invalid token","messagePattern":"invalid token","errorType":"exception","errorClass":"CanvasSecurity::ServicesJwt::InvalidRefresh","httpStatus":null,"severity":"warning","filePath":"gems/canvas_security/lib/canvas_security/services_jwt.rb","lineNumber":136,"sourceCode":"    end\n    if context\n      payload[:context_type] = context.class.name\n      payload[:context_id] = context.id.to_s\n    end\n    if audience\n      payload[:aud] = audience\n    end\n    if root_account_uuid\n      payload[:root_account_uuid] = root_account_uuid\n    end\n    generate(payload, base64:, symmetric:, encrypt:)\n  end\n\n  def self.refresh_for_user(jwt, domain, user, real_user: nil, symmetric: false)\n    begin\n      payload = new(jwt, wrapped: false).original_token(ignore_expiration: true)\n    rescue JSON::JWT::InvalidFormat\n      raise InvalidRefresh, \"invalid token\"\n    end\n\n    if refresh_invalid_for_user?(payload, domain, user, real_user)\n      raise InvalidRefresh, \"token does not match user and domain\"\n    end\n\n    if past_refresh_window?(payload[:exp])\n      raise InvalidRefresh, \"refresh window exceeded\"\n    end\n\n    if payload[:context_type].present?\n      context = payload[:context_type].constantize.find(payload[:context_id])\n    end\n\n    for_user(domain,\n             user,\n             real_user:,\n             workflows: payload[:workflows],","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_security/lib/canvas_security/services_jwt.rb#L118-L154","documentation":"ServicesJwt.refresh_for_user decodes the provided refresh token (ignoring expiration) to re-issue credentials. If the token string is not parseable as a JWT (JSON::JWT::InvalidFormat), it is re-raised as InvalidRefresh with message 'invalid token'; a valid-format token that doesn't match the user/domain raises the same class with a different message.","triggerScenarios":"Calling ServicesJwt.refresh_for_user(jwt, domain, user) where jwt is truncated, base64-corrupted, not a JWT at all, or signed/garbled data from a tampered request.","commonSituations":"Clients sending stale or manually mangled tokens; gateway/proxies truncating Authorization headers; storing tokens in cookies and losing characters through bad encoding.","solutions":["Inspect the incoming token string — verify it is a complete three-part JWT (header.payload.signature)","Fix the client to send the token unmodified (check header/cookie encoding and any proxy rewrites)","Rescue CanvasSecurity::ServicesJwt::InvalidRefresh and return 401 so the client re-authenticates","Verify you are passing the refresh token (not the wrapped access token) where required"],"exampleFix":"// before\nnew_jwt = CanvasSecurity::ServicesJwt.refresh_for_user(params[:refresh_token], domain, user)\n// after\nbegin\n  new_jwt = CanvasSecurity::ServicesJwt.refresh_for_user(params[:refresh_token], domain, user)\nrescue CanvasSecurity::ServicesJwt::InvalidRefresh\n  render json: { error: 'invalid refresh token' }, status: :unauthorized\nend","handlingStrategy":"try-catch","validationCode":"raise ArgumentError, 'not a jwt' unless token.is_a?(String) && token.count('.') == 2","typeGuard":"def jwt_shaped?(token) = token.is_a?(String) && token.split('.').length == 3","tryCatchPattern":"begin\n  CanvasSecurity::ServicesJwt.refresh_for_user(token, domain, user)\nrescue CanvasSecurity::ServicesJwt::InvalidRefresh\n  render json: { error: 'invalid token' }, status: :unauthorized\nend","preventionTips":["Validate token shape before sending to refresh_for_user","Return 401 on InvalidRefresh so clients re-authenticate","Check proxies/cookie encoding don't corrupt JWTs"],"tags":["ruby","jwt","token-validation"],"backgroundTag":"invalid-argument-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"}