{"record":{"id":"8b4811f12cbae8e1","repo":"instructure/canvas-lms","slug":"invalid-nonce-claim-in-id-token","errorCode":null,"errorMessage":"Invalid nonce claim in ID Token","messagePattern":"Invalid nonce claim in ID Token","errorType":"validation","errorClass":"OAuthValidationError","httpStatus":null,"severity":"error","filePath":"app/models/authentication_provider/open_id_connect.rb","lineNumber":295,"sourceCode":"          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\"]\n            id_token.merge!(userinfo)\n          end\n        end\n        id_token","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/authentication_provider/open_id_connect.rb#L277-L313","documentation":"Raised in claims() when the ID token's nonce claim does not equal the nonce Canvas stored in the OAuth transaction (token.options[:nonce]). Nonce binds the ID token to this specific authorization request, preventing token replay/injection; a mismatch means the token may not correspond to this login attempt.","triggerScenarios":"claims(token) during unique_id/persist_to_session/provider_attributes finds id_token['nonce'] != token.options[:nonce] — e.g. the IdP echoes a different nonce, the request's nonce was regenerated, or the IdP does not support nonce and omits/alters it.","commonSituations":"Non-compliant IdP that ignores the nonce request parameter; back-button/replayed authorization reusing an old code with a stale token; load-balanced Canvas where session data (nonce) is not shared across nodes; clock/issue where the auth request was started before a provider settings change.","solutions":["Verify the IdP echoes back the exact nonce sent in the authorization request (enable nonce support).","Clear stale OAuth state and start a fresh login (avoid replayed/back-button callbacks).","Ensure Canvas sessions are shared across all app nodes (Redis/shared session store) so token.options[:nonce] matches.","Test the raw ID token to see what nonce the IdP actually returned and correct the request flow."],"exampleFix":"# before: IdP configured without nonce support\nauthorization_url without nonce param -> token nonce nil\n// after: enable OIDC nonce in IdP client settings so the returned ID token includes the request nonce","handlingStrategy":"validation","validationCode":"payload = JSON.parse(Base64.urlsafe_decode64(id_token.split(\".\")[1]))\nraise \"nonce mismatch\" unless payload[\"nonce\"] == stored_nonce","typeGuard":null,"tryCatchPattern":"begin\n  provider.claims(token)\nrescue OAuthValidationError => e\n  Rails.logger.warn(\"OIDC nonce mismatch: #{e.message}\")\n  redirect_to login_path, alert: \"Login session expired; please try again.\"\nend","preventionTips":["Always send and verify nonce in authorization requests","Use a shared session store across app nodes","Discard stale OAuth state; never replay authorization codes","Confirm IdP supports nonce before enabling the provider"],"tags":["oidc","jwt","nonce","sso","replay-protection"],"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"}