instructure/canvas-lms · error · OAuthValidationError
Invalid JWT issuer
Error message
Invalid JWT issuer: %{issuer} What it means
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.
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.
Example fix
# before issuer: "https://idp.example.com" # after (match discovery-document issuer exactly) issuer: "https://idp.example.com/realms/main"
Defensive patterns
Strategy: validation
Validate before calling
discovery = JSON.parse(Net::HTTP.get(URI("#{idp_base}/.well-known/openid-configuration")))
payload = JSON.parse(Base64.urlsafe_decode64(id_token.split(".")[1]))
raise "issuer mismatch" unless discovery["issuer"] == payload["iss"] Try / catch
begin
provider.claims(token)
rescue OAuthValidationError => e
Rails.logger.error("OIDC issuer mismatch: #{e.message}")
redirect_to login_path, alert: "Login failed: unknown identity provider."
end Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid JWT audience
- Invalid nonce claim in ID Token
- Invalid signature
- Missing claims
- Failed to decode OpenID Connect back-channel logout token: #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/5bede514c7b3bdae.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/authentication_provider/open_id_connect.rb:291
token.options[:claims] ||= begin
id_token = unverified_id_token(token)
unless (missing_claims = %w[aud iss iat exp nonce] - id_token.keys).empty?
raise OAuthValidationError, t({ one: "Missing claim %{claims}", other: "Missing claims %{claims}" },
count: missing_claims.length,
claims: missing_claims.join(", "))
end
unless Array(id_token["aud"]).include?(client_id)
raise OAuthValidationError, t("Invalid JWT audience: %{audience}", audience: id_token["aud"].inspect)
end
if self.class.validate_issuer?
if issuer.blank?
raise OAuthValidationError, t("No issuer configured for OpenID Connect provider")
end
unless issuer === id_token["iss"] # rubocop:disable Style/CaseEquality -- may be a string or a RegEx
raise OAuthValidationError, t("Invalid JWT issuer: %{issuer}", issuer: id_token["iss"])
end
end
unless id_token["nonce"] == token.options[:nonce]
raise OAuthValidationError, t("Invalid nonce claim in ID Token")
end
if (signature_error = validate_signature(id_token))
raise OAuthValidationError, t("Invalid signature: %{signature_error}", signature_error:)
end
# we have a userinfo endpoint, and we don't have everything we want,
# then request more
if userinfo_endpoint.present? && !(requested_claims - id_token.keys).empty?
userinfo = token.get(userinfo_endpoint).parsed
debug_set(:userinfo, userinfo.to_json) if instance_debugging
# but only use it if it's for the user we logged in as
# see http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
if userinfo["sub"] == id_token["sub"]View on GitHub (pinned to 1c9f0bb801)