{"record":{"id":"b76c8576d3abb8c5","repo":"instructure/canvas-lms","slug":"cannot-generate-a-services-jwt-without-a-sub-entry","errorCode":null,"errorMessage":"Cannot generate a services JWT without a 'sub' entry","messagePattern":"Cannot generate a services JWT without a 'sub' entry","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"gems/canvas_security/lib/canvas_security/services_jwt.rb","lineNumber":161,"sourceCode":"    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\n  def self.create_payload(payload_data)\n    if payload_data[:sub].nil?\n      raise ArgumentError, \"Cannot generate a services JWT without a 'sub' entry\"\n    end\n\n    timestamp = Time.zone.now.to_i\n    payload_data.reverse_merge(\n      iss: CanvasSecurity.services_issuer,\n      aud: [DEFAULT_AUDIENCE],\n      exp: timestamp + 3600,  # token is good for 1 hour\n      nbf: timestamp - 30,    # don't accept the token in the past\n      iat: timestamp,         # tell when the token was issued\n      jti: SecureRandom.uuid # unique identifier\n    )\n  end\n\n  def self.decrypt(token, ignore_expiration: false)\n    CanvasSecurity.decrypt_encrypted_jwt(token,\n                                         {\n                                           \"HS256\" => [signing_secret, previous_signing_secret],\n                                           \"RS256\" => KeyStorage.public_keyset","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_security/lib/canvas_security/services_jwt.rb#L143-L179","documentation":"CanvasSecurity::ServicesJwt.create_payload raises ArgumentError when the payload_data hash passed in has a nil :sub (subject). The sub claim identifies the token's principal and is mandatory for services JWTs, so the library refuses to build a payload without it.","triggerScenarios":"Calling ServicesJwt.for_user / create_payload with a payload hash lacking :sub — e.g. user passed as nil (unauthenticated request, nil session user), or building payload_data manually without a sub key.","commonSituations":"Generating a token during a request where current_user is nil (public pages, API calls without auth); passing user.id vs user global_id mismatch resulting in nil; typos like payload[:subj] in custom wrappers.","solutions":["Ensure the user is resolved and non-nil before generating the token; redirect to login or return 401 for anonymous requests.","Pass the user's global_id as sub: sub: user.global_id (or use ServicesJwt.for_user which sets it).","Fix key typos — payload_data[:sub] must literally be the symbol :sub (string keys fail the check).","Add a guard in calling code: raise/return early when user.nil? before token creation."],"exampleFix":"// before\npayload = ServicesJwt.create_payload({}) # ArgumentError\n// after\nreturn render unauthorized unless user\npayload = ServicesJwt.create_payload({ sub: user.global_id, domain: request.host })","handlingStrategy":"validation","validationCode":"raise Authenticator::Unauthenticated, \"login required\" if user.nil?\npayload = CanvasSecurity::ServicesJwt.create_payload({ sub: user.global_id })","typeGuard":"def can_issue_services_token?(user)\n  !user.nil? && user.respond_to?(:global_id)\nend","tryCatchPattern":"begin\n  payload = CanvasSecurity::ServicesJwt.create_payload(payload_data)\nrescue ArgumentError => e\n  raise TokenIssuanceFailed, e.message\nend","preventionTips":["Check current_user presence before token generation","Always set sub to the user's global_id","Use ServicesJwt.for_user rather than raw create_payload","Validate payload keys with symbol keys only"],"tags":["jwt","missing-required-argument","authentication","services"],"backgroundTag":"missing-required-argument","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"}