instructure/canvas-lms · error · Canvas::OAuth::InvalidRequestError
@provider.error_message
Error message
@provider.error_message
What it means
Raised as Canvas::OAuth::InvalidRequestError with the provider's error_message after assertion_method_permitted? passes but @provider.valid? fails. For the symmetric provider, valid? is simply key.present? and the message is 'Unknown client_id'; other providers similarly surface a validation failure (bad JWT, bad secret, unknown key). This guards the client_credentials grant against unauthenticated or unrecognized clients.
Solutions
- Check the error_message returned with the response (e.g. 'Unknown client_id') and correct the client_id/client_secret in your client config.
- Confirm the DeveloperKey exists, is active, and lives on the account/shard you are authenticating against.
- For JWT assertion, re-sign the JWT with the public key registered on the developer key and check exp/iat/aud claims.
- Clear/wait out the developer-key cache (find_cached) after creating or updating the key, then retry.
Example fix
// before
{ client_id: '99000000000000', client_secret: 'old-secret' }
// after
{ client_id: '99000000000001', client_secret: '<rotated-secret>' } Defensive patterns
Strategy: try-catch
Validate before calling
key = DeveloperKey.find_cached(client_id) rescue nil abort 'Unknown client_id' unless key&.present?
Type guard
def known_client?(client_id) DeveloperKey.find_cached(client_id) rescue ActiveRecord::RecordNotFound nil end
Try / catch
begin token = exchange_client_credentials(opts) rescue Canvas::OAuth::InvalidRequestError => e handle_invalid_client(e.message) # e.g. 'Unknown client_id' end
Prevention
- Keep client_id/client_secret in config/env, never hardcode
- After rotating a secret, update all clients before deleting the old one
- Confirm the key exists on the account/shard you authenticate against
- For JWT, sign with the private key matching the key's registered public key and keep exp short
When it happens
Trigger: POST to /login/oauth2/token with grant_type=client_credentials where the client_id does not match an existing/cached DeveloperKey, the client_secret is wrong, or the JWT client_assertion fails signature/claims validation in the asymmetric provider.
Common situations: Typos or stale client_id/secret after rotating credentials; key deleted or moved to another account; key not cached yet (find_cached miss); JWT signed with the wrong private key or expired; environment pointing at a Canvas instance that lacks the key.
Related errors
- assertion method not supported for this grant_type
- invalid_client_id
- invalid_client_secret
- invalid_grant
- invalid_redirect
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/0ef1ff0fe0d37edf.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas/oauth/grant_types/client_credentials.rb:77
Canvas::OAuth::SymmetricClientCredentialsProvider.new(client_id, host, scopes: scopes_from_opts(opts), protocol:)
end
def secret_for(provider, opts)
provider.try(:secret) || opts[:client_secret]
end
def key_for(client_id)
DeveloperKey.find_cached(client_id)
rescue ::ActiveRecord::RecordNotFound
nil
end
def validate_type
unless @provider.assertion_method_permitted?
raise Canvas::OAuth::InvalidRequestError, "assertion method not supported for this grant_type"
end
raise Canvas::OAuth::InvalidRequestError, @provider.error_message unless @provider.valid?
raise Canvas::OAuth::InvalidScopeError, @provider.missing_scopes unless @provider.valid_scopes?
end
def generate_token
@provider.generate_token
end
def basic_auth?(opts)
opts[:client_assertion_type] != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
end
def scopes_from_opts(opts)
(opts[:scope] || opts[:scopes] || "").split
end
end
end
end
View on GitHub (pinned to 1c9f0bb801)