instructure/canvas-lms · error · SecretNotFound

either the tool proxy or developer key were not found

Error message

either the tool proxy or developer key were not found

What it means

The LTI OAuth2 validator derives the JWT signing secret by trying, in order: the active tool proxy's shared secret, the developer key's api_key, and a registration password from RegistrationRequestService. If none resolves to a present value it raises SecretNotFound, meaning the JWT cannot be verified because Canvas has no shared secret on record for the token's subject.

Solutions

  1. Confirm the JWT 'sub' exactly matches an active ToolProxy guid (check workflow_state = 'active' in the lti_tool_proxies table).
  2. Ensure the tool proxy's product_family has a developer key with a valid api_key.
  3. If this is an OIDC/registration request, verify RegistrationRequestService has a stored registration password for the context and sub.
  4. Reinstall/re-register the tool so a valid secret is provisioned, then retry the token request.

Example fix

// before (check in console)
Lti::ToolProxy.where(guid: sub, workflow_state: 'active').first # => nil
// after
Lti::ToolProxy.where(guid: sub).first.update!(workflow_state: 'active') # or fix the sub claim
Defensive patterns

Strategy: try-catch

Validate before calling

tp = Lti::ToolProxy.where(guid: jwt['sub'], workflow_state: 'active').first
dk = tp&.product_family&.developer_key
abort 'no secret on record' if tp.nil? && dk.nil? && reg_password.nil?

Type guard

def secret_available?(sub, context)
  Lti::ToolProxy.where(guid: sub, workflow_state: 'active').exists? ||
    Lti::DeveloperKey.where(id: nil).exists? # replace with actual key lookup
end

Try / catch

begin
  secret = validator.jwt_secret
rescue Lti::Oauth2::AuthorizationValidator::SecretNotFound => e
  render json: { error: 'unknown JWT subject' }, status: :unauthorized
end

Prevention

When it happens

Trigger: Decoding a signed JWT whose 'sub' matches no active ToolProxy, whose product family has no developer key with an api_key, and which does not correspond to a pending registration request with a stored reg_password.

Common situations: Tool proxy not yet activated (workflow_state != 'active') so the lookup misses; wrong guid/iss placed in the JWT sub claim; developer key deleted so its api_key is gone; client sending a JWT for a registration flow before the registration password exists.

Related errors


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

Appendix: source

Thrown at lib/lti/oauth2/authorization_validator.rb:94

          dev_key
        rescue ActiveRecord::RecordNotFound
          nil
        end
      end

      def sub
        tool_proxy&.guid || developer_key&.global_id || unverified_jwt[:sub]
      end

      private

      def jwt_secret
        secret = tool_proxy&.shared_secret
        secret ||= developer_key&.api_key
        secret ||= (RegistrationRequestService.retrieve_registration_password(@context, unverified_jwt[:sub]) || {})[:reg_password]
        return secret if secret.present?

        raise SecretNotFound, "either the tool proxy or developer key were not found"
      end

      def unverified_jwt
        @_unverified_jwt ||= begin
          decoded_jwt = JSON::JWT.decode(@raw_jwt, :skip_verification)
          decoded_jwt
        end
      end
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)