instructure/canvas-lms · error · InvalidAuthJwt
the Developer Key is not active or available in this…
Error message
the Developer Key is not active or available in this environment
What it means
During LTI OAuth2 JWT authorization, Canvas looks up the active ToolProxy by the JWT 'sub' (guid) and then fetches its ProductFamily's developer key. If a developer key exists but is not currently usable (inactive, banned, or unavailable in the current environment/shard), the validator rejects the auth request with InvalidAuthJwt. This prevents disabled or off-environment tools from obtaining access tokens even when their tool proxy record is still active.
Solutions
- Open Account > Developer Keys in the root account of the tool's product family and reactivate (or re-create) the key bound to the tool proxy.
- Verify the key exists on the same root account/shard the tool proxy belongs to; move or recreate it there if not.
- Check the developer key's workflow_state and expiry; restore it to 'active' via the UI or console if it was disabled.
- If the key is intentionally retired, reinstall the tool proxy with a new active key so a new tool proxy/key pair is created.
Example fix
// before (console check) tp = Lti::ToolProxy.where(guid: jwt_sub).first tp.product_family.developer_key.usable? # => false // after k = tp.product_family.developer_key k.update!(workflow_state: 'active') # or create a new usable key and reinstall the tool k.usable? # => true
Defensive patterns
Strategy: try-catch
Validate before calling
tp = Lti::ToolProxy.where(guid: sub, workflow_state: 'active').first key = tp&.product_family&.developer_key raise 'developer key unusable' if key.present? && !key.usable?
Type guard
def usable_key?(tool_proxy) key = tool_proxy&.product_family&.developer_key key.nil? || key.usable? end
Try / catch
begin
token = oauth2.request_access_token
rescue Lti::Oauth2::AuthorizationValidator::InvalidAuthJwt => e
Rails.logger.warn("LTI auth rejected: #{e.message}")
# surface 401 to the tool
end Prevention
- Keep the developer key active on the same root account/shard as the tool proxy.
- Monitor developer key workflow_state changes that affect installed tools.
- Test the token endpoint after any key rotation or environment migration.
When it happens
Trigger: POST to the LTI OAuth2 token endpoint with a JWT whose sub matches an active ToolProxy whose product_family.developer_key is present but DeveloperKey#usable? returns false (workflow_state not active, expired, deleted, or bound to a different account/environment).
Common situations: A developer key was deactivated or deleted in the account admin UI after the tool proxy was installed; the key exists only on another root account or shard so it is not usable in this environment; keys were rotated during an environment migration (beta/test vs production).
Related errors
- invalid_client_id
- invalid_grant
- Access token expired
- Access token invalid - signature likely incorrect
- Developer key mismatch
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/3b5f5f6d9bf449a6.
Report an issue: GitHub.
Appendix: source
Thrown at lib/lti/oauth2/authorization_validator.rb:60
def jwt
@_jwt ||= begin
validated_jwt = JSON::JWT.decode @raw_jwt, jwt_secret
validator = Canvas::Security::JwtValidator.new jwt: validated_jwt, expected_aud: @authorization_url, override_sub: sub
unless validator.valid?
raise InvalidAuthJwt, validator.error_message
end
validated_jwt
end
end
alias_method :validate!, :jwt
def tool_proxy
@tool_proxy ||=
if (tp = ToolProxy.where(guid: unverified_jwt[:sub], workflow_state: "active").first)
developer_key = tp.product_family.developer_key
raise InvalidAuthJwt, "the Developer Key is not active or available in this environment" if developer_key.present? && !developer_key.usable?
ims_tool_proxy = ::IMS::LTI::Models::ToolProxy.from_json(tp.raw_data)
unless ims_tool_proxy.enabled_capabilities.intersect?(["Security.splitSecret", "OAuth.splitSecret"])
raise InvalidAuthJwt, "the Tool Proxy must be using a split secret"
end
tp
end
end
def developer_key
@_developer_key ||= begin
dev_key = DeveloperKey.find_cached(unverified_jwt[:sub])
raise MissingAuthorizationCode if dev_key && @code.blank?
dev_key
rescue ActiveRecord::RecordNotFound
nilView on GitHub (pinned to 1c9f0bb801)