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
- 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.
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
- 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
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
- Attachment verifier token expired: #
- Attachment verifier token id mismatch. token id: #
- masquerading user not found
- refresh window exceeded
- requestor_user is required for JWT generation
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_keysetView on GitHub (pinned to 1c9f0bb801)