instructure/canvas-lms · error · OAuthValidationError
Failed to decode OpenID Connect id_token: #
Error message
Failed to decode OpenID Connect id_token: #{jwt_string.inspect} What it means
During OpenID Connect login the provider decodes the returned id_token with verification skipped (to read claims). If the string cannot be decoded as a JWT at all (InvalidToken/TokenExpired even with skip_verification), it logs this warning and raises OAuthValidationError, aborting the login. This means the identity provider returned something that is not a usable JWT.
Solutions
- Inspect what the IdP actually returns at the token endpoint (log the raw response body) and fix the issuer configuration (token URL, client_id/secret)
- Confirm the provider is an OIDC-compliant issuer that returns a JWT id_token (openid scope, correct response_type)
- Check for captive portals/proxies rewriting responses to HTML
- Update/re-test the authentication provider config in Canvas after IdP-side changes
Example fix
// before
provider.id_token = response.parsed_response['id_token'] rescue nil # nil/HTML slips through
// after
token = response.parsed_response&.dig('id_token')
raise OAuthValidationError, 'no id_token from IdP' unless token.is_a?(String) && token.count('.') == 2 Defensive patterns
Strategy: try-catch
Validate before calling
def plausible_jwt?(s)
s.is_a?(String) && s.count('.') == 2 && s.length > 32
end
# check response.parsed_response&.dig('id_token') before handing to decode_jwt Type guard
def jwt_like?(token)
token.is_a?(String) && token.split('.').length == 3
end Try / catch
begin
id_token = Canvas::Security.decode_jwt(jwt_string, [:skip_verification])
rescue Canvas::Security::InvalidToken, Canvas::Security::TokenExpired => e
raise OAuthValidationError, "IdP returned unusable id_token: #{e.class}"
end Prevention
- Verify the IdP's token endpoint URL and client credentials in the authentication provider config
- Log the raw token endpoint response when debugging SSO failures
- Confirm the provider returns an OIDC-compliant JWT id_token (not an opaque token)
- Test the IdP integration after any provider-side configuration change
When it happens
Trigger: The OIDC token endpoint returns an id_token that is blank, opaque, HTML (an error page), or otherwise not a JWT during the authorization-code exchange.
Common situations: Misconfigured issuer (token endpoint returns an error payload with 200); provider returning non-JWT opaque tokens; network proxies injecting HTML error pages; wrong client credentials causing an error response parsed as a token; provider outage.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Failed to decode OpenID Connect back-channel logout token: #
- Invalid JWT audience
- Invalid JWT issuer
- Invalid nonce claim in ID Token
- Invalid signature
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/d972a2a032728d9a.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/authentication_provider/open_id_connect.rb:342
super.tap do |options|
case token_endpoint_auth_method
when "client_secret_basic"
options[:auth_scheme] = :basic_auth
when "client_secret_post"
options[:auth_scheme] = :request_body
end
end
end
def unverified_id_token(token)
jwt_string = token.options[:jwt_string] = token.params["id_token"] || token.token
debug_set(:id_token, jwt_string) if instance_debugging
id_token = {} if jwt_string.blank?
id_token ||= begin
::Canvas::Security.decode_jwt(jwt_string, [:skip_verification])
rescue ::Canvas::Security::InvalidToken, ::Canvas::Security::TokenExpired => e
Rails.logger.warn("Failed to decode OpenID Connect id_token: #{jwt_string.inspect}")
raise OAuthValidationError, e.message
end
debug_set(:header, id_token.header.to_json) if instance_debugging
debug_set(:claims, id_token.to_json) if instance_debugging
id_token
end
private
def download_jwks(force: false)
if jwks_uri.blank?
self.jwks = nil
return
end
return unless force || settings["jwks"].nil? || jwks_uri_changed?
View on GitHub (pinned to 1c9f0bb801)