{"record":{"id":"d972a2a032728d9a","repo":"instructure/canvas-lms","slug":"failed-to-decode-openid-connect-id-token-jwt-string-inspect","errorCode":null,"errorMessage":"Failed to decode OpenID Connect id_token: #{jwt_string.inspect}","messagePattern":"Failed to decode OpenID Connect id_token: #(.+?)","errorType":"console","errorClass":"OAuthValidationError","httpStatus":null,"severity":"error","filePath":"app/models/authentication_provider/open_id_connect.rb","lineNumber":342,"sourceCode":"      super.tap do |options|\n        case token_endpoint_auth_method\n        when \"client_secret_basic\"\n          options[:auth_scheme] = :basic_auth\n        when \"client_secret_post\"\n          options[:auth_scheme] = :request_body\n        end\n      end\n    end\n\n    def unverified_id_token(token)\n      jwt_string = token.options[:jwt_string] = token.params[\"id_token\"] || token.token\n      debug_set(:id_token, jwt_string) if instance_debugging\n      id_token = {} if jwt_string.blank?\n\n      id_token ||= begin\n        ::Canvas::Security.decode_jwt(jwt_string, [:skip_verification])\n      rescue ::Canvas::Security::InvalidToken, ::Canvas::Security::TokenExpired => e\n        Rails.logger.warn(\"Failed to decode OpenID Connect id_token: #{jwt_string.inspect}\")\n        raise OAuthValidationError, e.message\n      end\n\n      debug_set(:header, id_token.header.to_json) if instance_debugging\n      debug_set(:claims, id_token.to_json) if instance_debugging\n\n      id_token\n    end\n\n    private\n\n    def download_jwks(force: false)\n      if jwks_uri.blank?\n        self.jwks = nil\n        return\n      end\n      return unless force || settings[\"jwks\"].nil? || jwks_uri_changed?\n","sourceCodeStart":324,"sourceCodeEnd":360,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/authentication_provider/open_id_connect.rb#L324-L360","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nprovider.id_token = response.parsed_response['id_token'] rescue nil # nil/HTML slips through\n// after\ntoken = response.parsed_response&.dig('id_token')\nraise OAuthValidationError, 'no id_token from IdP' unless token.is_a?(String) && token.count('.') == 2","handlingStrategy":"try-catch","validationCode":"def plausible_jwt?(s)\n  s.is_a?(String) && s.count('.') == 2 && s.length > 32\nend\n# check response.parsed_response&.dig('id_token') before handing to decode_jwt","typeGuard":"def jwt_like?(token)\n  token.is_a?(String) && token.split('.').length == 3\nend","tryCatchPattern":"begin\n  id_token = Canvas::Security.decode_jwt(jwt_string, [:skip_verification])\nrescue Canvas::Security::InvalidToken, Canvas::Security::TokenExpired => e\n  raise OAuthValidationError, \"IdP returned unusable id_token: #{e.class}\"\nend","preventionTips":["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"],"tags":["oauth","openid-connect","jwt","sso"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}