instructure/canvas-lms · error · ArgumentError

Cannot generate a services JWT without a 'sub' entry

Error message

Cannot generate a services JWT without a 'sub' entry

What it means

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.

Solutions

  1. Ensure the user is resolved and non-nil before generating the token; redirect to login or return 401 for anonymous requests.
  2. Pass the user's global_id as sub: sub: user.global_id (or use ServicesJwt.for_user which sets it).
  3. Fix key typos — payload_data[:sub] must literally be the symbol :sub (string keys fail the check).
  4. Add a guard in calling code: raise/return early when user.nil? before token creation.

Example fix

// before
payload = ServicesJwt.create_payload({}) # ArgumentError
// after
return render unauthorized unless user
payload = ServicesJwt.create_payload({ sub: user.global_id, domain: request.host })
Defensive patterns

Strategy: validation

Validate before calling

raise Authenticator::Unauthenticated, "login required" if user.nil?
payload = CanvasSecurity::ServicesJwt.create_payload({ sub: user.global_id })

Type guard

def can_issue_services_token?(user)
  !user.nil? && user.respond_to?(:global_id)
end

Try / catch

begin
  payload = CanvasSecurity::ServicesJwt.create_payload(payload_data)
rescue ArgumentError => e
  raise TokenIssuanceFailed, e.message
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/b76c8576d3abb8c5. Report an issue: GitHub.

Appendix: source

Thrown at gems/canvas_security/lib/canvas_security/services_jwt.rb:161

    if past_refresh_window?(payload[:exp])
      raise InvalidRefresh, "refresh window exceeded"
    end

    if payload[:context_type].present?
      context = payload[:context_type].constantize.find(payload[:context_id])
    end

    for_user(domain,
             user,
             real_user:,
             workflows: payload[:workflows],
             context:,
             symmetric:)
  end

  def self.create_payload(payload_data)
    if payload_data[:sub].nil?
      raise ArgumentError, "Cannot generate a services JWT without a 'sub' entry"
    end

    timestamp = Time.zone.now.to_i
    payload_data.reverse_merge(
      iss: CanvasSecurity.services_issuer,
      aud: [DEFAULT_AUDIENCE],
      exp: timestamp + 3600,  # token is good for 1 hour
      nbf: timestamp - 30,    # don't accept the token in the past
      iat: timestamp,         # tell when the token was issued
      jti: SecureRandom.uuid # unique identifier
    )
  end

  def self.decrypt(token, ignore_expiration: false)
    CanvasSecurity.decrypt_encrypted_jwt(token,
                                         {
                                           "HS256" => [signing_secret, previous_signing_secret],
                                           "RS256" => KeyStorage.public_keyset

View on GitHub (pinned to 1c9f0bb801)