{"record":{"id":"5bede514c7b3bdae","repo":"instructure/canvas-lms","slug":"invalid-jwt-issuer-issuer","errorCode":null,"errorMessage":"Invalid JWT issuer: %{issuer}","messagePattern":"Invalid JWT issuer: %(.+?)","errorType":"validation","errorClass":"OAuthValidationError","httpStatus":null,"severity":"error","filePath":"app/models/authentication_provider/open_id_connect.rb","lineNumber":291,"sourceCode":"      token.options[:claims] ||= begin\n        id_token = unverified_id_token(token)\n\n        unless (missing_claims = %w[aud iss iat exp nonce] - id_token.keys).empty?\n          raise OAuthValidationError, t({ one: \"Missing claim %{claims}\", other: \"Missing claims %{claims}\" },\n                                        count: missing_claims.length,\n                                        claims: missing_claims.join(\", \"))\n        end\n\n        unless Array(id_token[\"aud\"]).include?(client_id)\n          raise OAuthValidationError, t(\"Invalid JWT audience: %{audience}\", audience: id_token[\"aud\"].inspect)\n        end\n\n        if self.class.validate_issuer?\n          if issuer.blank?\n            raise OAuthValidationError, t(\"No issuer configured for OpenID Connect provider\")\n          end\n          unless issuer === id_token[\"iss\"] # rubocop:disable Style/CaseEquality -- may be a string or a RegEx\n            raise OAuthValidationError, t(\"Invalid JWT issuer: %{issuer}\", issuer: id_token[\"iss\"])\n          end\n        end\n        unless id_token[\"nonce\"] == token.options[:nonce]\n          raise OAuthValidationError, t(\"Invalid nonce claim in ID Token\")\n        end\n\n        if (signature_error = validate_signature(id_token))\n          raise OAuthValidationError, t(\"Invalid signature: %{signature_error}\", signature_error:)\n        end\n\n        # we have a userinfo endpoint, and we don't have everything we want,\n        # then request more\n        if userinfo_endpoint.present? && !(requested_claims - id_token.keys).empty?\n          userinfo = token.get(userinfo_endpoint).parsed\n          debug_set(:userinfo, userinfo.to_json) if instance_debugging\n          # but only use it if it's for the user we logged in as\n          # see http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse\n          if userinfo[\"sub\"] == id_token[\"sub\"]","sourceCodeStart":273,"sourceCodeEnd":309,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/authentication_provider/open_id_connect.rb#L273-L309","documentation":"Raised in claims() when validate_issuer? is enabled and the ID token's iss claim does not match the configured issuer. Canvas uses case equality (===) so the configured issuer may be a String or RegEx; any mismatch means the token was issued by an unexpected authority and is rejected.","triggerScenarios":"claims() runs during login with a configured issuer; issuer === id_token['iss'] is false — e.g. configured 'https://idp.example.com' but token says 'https://idp.example.com/realms/main' or uses a different host/port/scheme, or internal vs external URL differs behind a proxy.","commonSituations":"IdP issuer URL includes a realm/path or trailing difference not reflected in the Canvas config; load balancer terminates TLS so token iss has http vs https mismatch; IdP upgraded and changed its issuer URL (e.g. moved to a new realm path); admin copied the well-known endpoint URL instead of the issuer value.","solutions":["Copy the issuer value exactly from the IdP's discovery document (.well-known/openid-configuration 'issuer' field) into the provider settings.","Compare the iss value in the error message to the configured issuer and fix scheme/host/port/path/trailing-slash differences.","Use a RegEx issuer in Canvas settings if the IdP serves multiple valid issuer URLs.","Update the config after IdP migrations that change the issuer URL."],"exampleFix":"# before\nissuer: \"https://idp.example.com\"\n# after (match discovery-document issuer exactly)\nissuer: \"https://idp.example.com/realms/main\"","handlingStrategy":"validation","validationCode":"discovery = JSON.parse(Net::HTTP.get(URI(\"#{idp_base}/.well-known/openid-configuration\")))\npayload = JSON.parse(Base64.urlsafe_decode64(id_token.split(\".\")[1]))\nraise \"issuer mismatch\" unless discovery[\"issuer\"] == payload[\"iss\"]","typeGuard":null,"tryCatchPattern":"begin\n  provider.claims(token)\nrescue OAuthValidationError => e\n  Rails.logger.error(\"OIDC issuer mismatch: #{e.message}\")\n  redirect_to login_path, alert: \"Login failed: unknown identity provider.\"\nend","preventionTips":["Copy the issuer exactly from .well-known/openid-configuration","Account for realm paths and trailing slashes","Re-check issuer after IdP upgrades/migrations","Use a RegEx issuer only when the IdP legitimately serves multiple values"],"tags":["oidc","jwt","sso","issuer-mismatch"],"backgroundTag":"unexpected-response-shape","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"}