{"record":{"id":"7f5df5126e77b7c3","repo":"instructure/canvas-lms","slug":"validator-error-message","errorCode":null,"errorMessage":"#{validator.error_message}","messagePattern":"#\\{validator\\.error_message\\}","errorType":"validation","errorClass":"InvalidAuthJwt","httpStatus":401,"severity":"error","filePath":"lib/lti/oauth2/authorization_validator.rb","lineNumber":47,"sourceCode":"      class InvalidAuthJwt < StandardError\n      end\n\n      class MissingAuthorizationCode < StandardError\n      end\n\n      def initialize(jwt:, authorization_url:, code: nil, context:)\n        @raw_jwt = jwt\n        @authorization_url = authorization_url\n        @code = code\n        @context = context\n      end\n\n      def jwt\n        @_jwt ||= begin\n          validated_jwt = JSON::JWT.decode @raw_jwt, jwt_secret\n          validator = Canvas::Security::JwtValidator.new jwt: validated_jwt, expected_aud: @authorization_url, override_sub: sub\n          unless validator.valid?\n            raise InvalidAuthJwt, validator.error_message\n          end\n\n          validated_jwt\n        end\n      end\n\n      alias_method :validate!, :jwt\n\n      def tool_proxy\n        @tool_proxy ||=\n          if (tp = ToolProxy.where(guid: unverified_jwt[:sub], workflow_state: \"active\").first)\n            developer_key = tp.product_family.developer_key\n            raise InvalidAuthJwt, \"the Developer Key is not active or available in this environment\" if developer_key.present? && !developer_key.usable?\n\n            ims_tool_proxy = ::IMS::LTI::Models::ToolProxy.from_json(tp.raw_data)\n            unless ims_tool_proxy.enabled_capabilities.intersect?([\"Security.splitSecret\", \"OAuth.splitSecret\"])\n              raise InvalidAuthJwt, \"the Tool Proxy must be using a split secret\"\n            end","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/lti/oauth2/authorization_validator.rb#L29-L65","documentation":"Lti::OAuth2::AuthorizationValidator#jwt decodes the posted OIDC/authorization-request JWT with the tool's shared secret, then runs it through Canvas::Security::JwtValidator with the expected audience set to the authorization URL. If validation fails (wrong aud, missing/expired claims, etc.), InvalidAuthJwt is raised with the validator's error_message as the message.","triggerScenarios":"Third-party tool POSTing an authorization-request (OIDC launch) JWT whose claims fail JwtValidator checks - typically aud not matching the Canvas authorization_url, missing sub/iat/exp, or stale iat.","commonSituations":"Tools configured with the wrong Canvas login/authorization URL, missing openid-configuration discovery, custom launch flows omitting claims Canvas requires, and environment/domain mismatches between the tool config and the Canvas host receiving the launch.","solutions":["Read validator.error_message in the raised exception to see the exact failing claim and fix it in the tool's JWT","Set the JWT aud to the Canvas OIDC authorization endpoint URL configured for the tool","Ensure required claims (iss, sub, iat, exp, aud) are present and temporally valid","Verify the tool platform config points at the correct Canvas instance/authorization URL"],"exampleFix":"// before\nconst payload = {iss: clientId, aud: 'https://tool.example.com/callback', sub: clientId, iat, exp};\n// after\nconst payload = {iss: clientId, aud: 'https://canvas.example.com/api/lti/authorize_redirect', sub: clientId, iat, exp};","handlingStrategy":"validation","validationCode":"const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());\nassert(payload.aud === authorizationUrl, 'aud must be the Canvas authorization URL');\nassert(payload.iss && payload.sub && payload.iat && payload.exp, 'required claims missing');\nassert(payload.exp > Date.now()/1000, 'jwt expired');","typeGuard":"function authJwtLooksValid(jwt, authorizationUrl) {\n  try {\n    const p = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());\n    const audOk = Array.isArray(p.aud) ? p.aud.includes(authorizationUrl) : p.aud === authorizationUrl;\n    return audOk && !!p.iss && !!p.sub && p.exp > Date.now() / 1000;\n  } catch { return false; }\n}","tryCatchPattern":"begin\n  validator.jwt\nrescue Lti::OAuth2::AuthorizationValidator::InvalidAuthJwt => e\n  Rails.logger.warn(\"auth jwt invalid: #{e.message}\")\n  render json: {error: 'invalid_request'}, status: :bad_request\nend","preventionTips":["Point tools at the correct Canvas OIDC authorization URL and use it as aud","Include iss, sub, iat, exp in every authorization request JWT","Re-fetch platform OpenID configuration when Canvas domains change","Log validator.error_message server-side to pinpoint failing claims"],"tags":["lti","oidc","jwt","claims-validation"],"backgroundTag":"schema-validation-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"}