instructure/canvas-lms · error · Canvas::OAuth::RequestError
invalid_client_secret
invalid_client_secret
Error message
invalid_client_secret
What it means
Canvas::OAuth::RequestError :invalid_client_secret is raised in BaseType#validate_client_id_and_secret when the client_id is valid but @provider.is_authorized_by?(@secret) fails — the supplied client_secret does not match the developer key's secret (or a valid Canvas-issued access token). The error is skipped only when the grant type allows public clients, the key is marked public_client?, and the secret is blank.
Solutions
- Re-copy the current client_secret from Canvas account > Developer Keys and update the app's configuration/env.
- Confirm client_id and client_secret belong to the same developer key.
- If the client is truly public (SPA/mobile), mark the developer key as a public client in Canvas and omit the secret entirely.
- Check for env-var issues: trailing whitespace, missing quotes, or stale secrets in deploy environments.
Example fix
// before: stale secret after rotation
body: { grant_type: 'authorization_code', code, client_id, client_secret: OLD_SECRET }
// after: read the rotated secret from config, or omit it for a public client
body: { grant_type: 'authorization_code', code, client_id, client_secret: process.env.CANVAS_CLIENT_SECRET } Defensive patterns
Strategy: try-catch
Validate before calling
function hasSecretOrPublicClient(cfg) { return (typeof cfg.clientSecret === 'string' && cfg.clientSecret.trim() !== '') || cfg.isPublicClient === true } Type guard
function hasClientSecret(cfg) { return typeof cfg.clientSecret === 'string' && cfg.clientSecret.trim() !== '' } Try / catch
try {
token = await exchangeCode(code, clientId, clientSecret)
} catch (e) {
if (e.body?.error === 'invalid_client_secret') {
throw new Error('client_secret rejected: rotate/redeploy secret from Canvas Developer Keys')
}
throw e
} Prevention
- Store client_secret in a secret manager and redeploy immediately after rotating it in Canvas.
- Trim and quote secret values in env files to avoid whitespace damage.
- Keep client_id and client_secret as a single versioned credential pair.
- Use public_client? keys (no secret) only for genuine public clients.
When it happens
Trigger: POST to /login/oauth2/token with a correct client_id but a wrong, stale, or omitted client_secret on a key that is not a public client; sending a secret belonging to a different key; using client_credentials/authorization_code grant with a key flagged public_client? while also sending a non-matching secret.
Common situations: Secret rotated in the Canvas admin UI but the app still deploys the old value; whitespace/quoting damage when storing the secret in env vars; mixing credentials of two keys (right client_id, other key's secret); expecting public-client behavior without marking the key public_client? in Canvas.
Related errors
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/d17f3fe77220913a.
Report an issue: GitHub.
Appendix: source
Thrown at lib/canvas/oauth/grant_types/base_type.rb:39
# allow public clients as defined in RFC 6749.
def allow_public_client?
false
end
def supported_type?
false
end
private
def validate_client_id_and_secret
raise Canvas::OAuth::RequestError, :invalid_client_id unless @provider.has_valid_key?
# Issue an access token if the grant type supports public client and the
# DeveloperKey identifies a public client. Otherwise, the client must must
# provide a client secret.
return if allow_public_client? && @provider.key&.public_client? && @secret.blank?
raise Canvas::OAuth::RequestError, :invalid_client_secret unless @provider.is_authorized_by?(@secret)
end
def validate_type
raise "Abstract Method"
end
def generate_token
raise "Abstract Method"
end
end
end
end
View on GitHub (pinned to 1c9f0bb801)