{"record":{"id":"1d7e4576972a7039","repo":"instructure/canvas-lms","slug":"token-does-not-match-user-and-domain","errorCode":null,"errorMessage":"token does not match user and domain","messagePattern":"token does not match user and domain","errorType":"exception","errorClass":"CanvasSecurity::ServicesJwt::InvalidRefresh","httpStatus":null,"severity":"error","filePath":"gems/canvas_security/lib/canvas_security/services_jwt.rb","lineNumber":140,"sourceCode":"    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],\n             context:,\n             symmetric:)\n  end\n","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_security/lib/canvas_security/services_jwt.rb#L122-L158","documentation":"CanvasSecurity::ServicesJwt raises InvalidRefresh with this message when refreshing a services JWT: the decoded token's subject/context does not match the user (and masqueraded real_user) or the request domain supplied to the refresh call. It guards against using a token issued for a different user or host to mint a new one, preventing token confusion and cross-domain replay.","triggerScenarios":"Calling ServicesJwt.for_user(user, domain: ...).refresh(token, ...) (or the module-level refresh wrapper) with a JWT whose :sub is not the given user's global_id, whose :real_user does not match real_user, or whose :aud/:domainhostname does not match the domain argument.","commonSituations":"Passing a token issued for an admin while refreshing as the impersonated (real) user; swapping tokens between dev/test hosts where the domain hostname differs; caching a token per session but resolving the current user from a different shard or login; copying a token into a service that calls back with its own domain.","solutions":["Ensure the user, real_user, and domain arguments passed to refresh are exactly those the token was originally created with (check payload sub/real_user_id/domainhostname).","Regenerate the token from scratch with ServicesJwt.for_user(correct_user, domain: correct_domain) instead of refreshing a mismatched one.","Verify the domain string matches the token's aud/hostname entry, including subdomain and port, e.g. canvas.dev.local vs canvas.test.local.","If impersonating, confirm real_user is set the same way at creation (as_user/real_user fields) as at refresh.","Inspect the payload (ServicesJwt.new(token).original_token(ignore_expiration: true)) to compare sub, real_user_id, and aud against your arguments."],"exampleFix":"// before\nServicesJwt.refresh(stale_token, domain: \"canvas.example.com\", user: other_user)\n// after\njwt = CanvasSecurity::ServicesJwt.for_user(original_user, domain: original_domain)\nnew_token = CanvasSecurity::ServicesJwt.refresh(token, domain: original_domain, user: original_user)","handlingStrategy":"try-catch","validationCode":"token = CanvasSecurity::ServicesJwt.new(jwt).original_token(ignore_expiration: true)\nraise TokenMismatch unless token[:sub] == user.global_id.to_s\ncorrect = (token[:aud] || []).include?(domain) || token[:domainhostname] == domain","typeGuard":"def valid_services_token?(payload, user, domain)\n  payload.is_a?(Hash) && payload[:sub] == user.global_id.to_s &&\n    (payload[:domainhostname] == domain || Array(payload[:aud]).include?(domain))\nend","tryCatchPattern":"begin\n  new_token = CanvasSecurity::ServicesJwt.refresh(jwt, domain: domain, user: user, real_user: real_user)\nrescue CanvasSecurity::ServicesJwt::InvalidRefresh => e\n  Rails.logger.warn(\"token refresh rejected: #{e.message}; reissuing\")\n  new_token = CanvasSecurity::ServicesJwt.for_user(user, domain: domain).token\nend","preventionTips":["Always refresh with the exact user/domain pair used at token creation","Store domain alongside the cached token","Handle masquerade by persisting real_user with the token","Reissue instead of refreshing when in doubt"],"tags":["jwt","authentication","token-refresh","security"],"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"}