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

  1. 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.
  2. Verify the key exists on the same root account/shard the tool proxy belongs to; move or recreate it there if not.
  3. Check the developer key's workflow_state and expiry; restore it to 'active' via the UI or console if it was disabled.
  4. 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

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


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
          nil

View on GitHub (pinned to 1c9f0bb801)