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
- Confirm the JWT 'sub' exactly matches an active ToolProxy guid (check workflow_state = 'active' in the lti_tool_proxies table).
- Ensure the tool proxy's product_family has a developer key with a valid api_key.
- If this is an OIDC/registration request, verify RegistrationRequestService has a stored registration password for the context and sub.
- 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
- Verify the JWT sub claim matches the tool proxy guid exactly.
- Ensure tool proxies are activated after registration.
- Do not delete developer keys still referenced by active tool proxies.
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
- Access token expired
- Access token invalid - signature likely incorrect
- iat must be in the past
- Invalid access token field/s: #
- invalid aud
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)