instructure/canvas-lms · error
Failed to decode OpenID Connect back-channel logout token: #
Error message
Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect} What it means
OpenID Connect back-channel logout sends a signed `logout_token` JWT. Canvas decodes it with verification skipped just to inspect claims; if the string is not a structurally decodable JWT, Canvas::Security::InvalidToken is raised, the controller logs this warning, renders 400 'Invalid logout token', and increments an :invalid_token failure stat.
Solutions
- Verify the IdP sends an unencrypted signed JWS logout_token (or configure decryption if your setup supports JWE)
- Inspect jwt_string in the log — confirm the param is populated and not truncated by proxies or request limits
- Confirm the client posts logout_token as a form param on the back-channel logout URL
- Check Canvas::Security key configuration (JWK/signing keys) matches the issuer
Example fix
// before curl -X POST $HOST/login/openid_connect/logout -d 'token=...' // after curl -X POST $HOST/login/openid_connect/logout -d 'logout_token=eyJhbGciOi...'
Defensive patterns
Strategy: validation
Validate before calling
logout_token = params['logout_token']
valid = logout_token.present? && logout_token.split('.').length == 3 Type guard
def jwt_like?(str)
str.is_a?(String) && str.split('.').length == 3 && str.match?(/\A[\w-]+\.[\w-]+\.[\w-]*\z/)
end Try / catch
begin token = Canvas::Security.decode_jwt(jwt_string, [:skip_verification]) rescue Canvas::Security::InvalidToken render plain: 'Invalid logout token', status: :bad_request end
Prevention
- Ensure the IdP back-channel logout posts logout_token as a form param
- Confirm the IdP emits signed JWS (not encrypted JWE) tokens
- Check proxies don't truncate large POST bodies
- Log token length/shape (not content) for diagnosis
When it happens
Trigger: POST to the OpenID Connect back-channel logout endpoint where params['logout_token'] is missing, empty, truncated, or not a base64url JWS (decode_jwt with [:skip_verification] still requires valid JWT structure).
Common situations: IdP misconfigured to send an encrypted JWE the decoder can't parse; token truncated by a proxy; client posting the token under the wrong param name; non-OP callers hitting the endpoint.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
- 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 id_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/8624d53708cdebc8.
Report an issue: GitHub.
Appendix: source
Thrown at app/controllers/login/openid_connect_controller.rb:63
unless Pseudonym.expire_oidc_session(logout_token, request)
increment_statsd(:failure, reason: :no_session)
return render plain: "No session found", status: :not_found
end
increment_statsd(:success)
render plain: "OK", status: :ok
end
def validate_logout_token
# NOT SUPPORTED without redis
return render plain: "NOT SUPPORTED", status: :method_not_allowed unless Canvas.redis_enabled?
jwt_string = request.request_parameters["logout_token"]
logout_token = begin
::Canvas::Security.decode_jwt(jwt_string, [:skip_verification])
rescue ::Canvas::Security::InvalidToken
Rails.logger.warn("Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect}")
render plain: "Invalid logout token", status: :bad_request
increment_statsd(:failure, reason: :invalid_token)
nil
end
return unless logout_token
unless (missing_claims = %w[aud iss iat exp events jti] - logout_token.keys).empty?
render plain: "Missing claim#{"s" if missing_claims.length > 1} #{missing_claims.join(", ")}",
status: :bad_request
increment_statsd(:failure, reason: :missing_claims)
return
end
unless logout_token["events"].is_a?(Hash) && logout_token["events"][OIDC_BACKCHANNEL_LOGOUT_EVENT_URN].is_a?(Hash)
render plain: "Invalid events", status: :bad_request
increment_statsd(:failure, reason: :invalid_events)
return
end
unless logout_token["sid"] || logout_token["sub"]View on GitHub (pinned to 1c9f0bb801)