{"record":{"id":"54856a4e458517c5","repo":"instructure/canvas-lms","slug":"invalid-access-token-field-s-validator-error-message","errorCode":null,"errorMessage":"Invalid access token field/s: #{validator.error_message}","messagePattern":"Invalid access token field/s: #(.+?)","errorType":"exception","errorClass":"AdvantageErrors::InvalidAccessTokenClaims","httpStatus":401,"severity":"error","filePath":"lib/lti/ims/advantage_access_token.rb","lineNumber":66,"sourceCode":"      rescue AdvantageErrors::AdvantageServiceError\n        raise\n      rescue => e\n        raise AdvantageErrors::AdvantageServiceError, e\n      end\n\n      def validate_claims!(expected_audience)\n        validator = Canvas::Security::JwtValidator.new(\n          jwt: decoded_jwt,\n          expected_aud: expected_audience,\n          require_iss: true,\n          skip_jti_check: true,\n          max_iat_age: 60.minutes\n        )\n\n        # In this case we know the error message can just be safely shunted into the API response (in other cases\n        # we're more wary about leaking impl details)\n        unless validator.valid?\n          raise AdvantageErrors::InvalidAccessTokenClaims.new(\n            nil,\n            api_message: \"Invalid access token field/s: #{validator.error_message}\"\n          )\n        end\n      end\n\n      def claim(name)\n        decoded_jwt[name]\n      end\n\n      def decoded_jwt\n        @_decoded_jwt = Canvas::Security.decode_jwt(@raw_jwt_str)\n      end\n\n      def client_id\n        claim(\"sub\")\n      end\n    end","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/lti/ims/advantage_access_token.rb#L48-L84","documentation":"AdvantageAccessToken#validate_claims! runs the decoded JWT through Canvas::Security::JwtValidator with require_iss, a 60-minute max iat age, an expected audience, and jti check skipped. If any claim fails validation, the validator's error_message (considered safe to expose) is raised verbatim as AdvantageErrors::InvalidAccessTokenClaims with 'Invalid access token field/s: ...'.","triggerScenarios":"Client_credentials tokens posted to NRPS/AGS endpoints with a wrong aud (not the Canvas OAuth2 token URL), missing iss, an iat older than 60 minutes, or otherwise malformed claims.","commonSituations":"Using the wrong audience URL (e.g. account-scoped vs canvas.instructure.com), reusing a token generated more than an hour ago, tool SDKs omitting iss, or pointing tools at a different Canvas host than the one minting tokens.","solutions":["Read the specific field named in the message and correct that claim in the token request","Set aud exactly to the Canvas OAuth2 token endpoint URL used in the request","Ensure iat is current (within 60 minutes) and iss is present and correct","Regenerate the token rather than replaying a cached one"],"exampleFix":"// before: wrong audience\nconst payload = {iss: clientId, sub: clientId, aud: 'https://canvas.example.com', iat: oldIat, exp};\n// after\nconst payload = {iss: clientId, sub: clientId, aud: 'https://canvas.example.com/login/oauth2/token', iat: Math.floor(Date.now()/1000), exp: Math.floor(Date.now()/1000)+3600};","handlingStrategy":"validation","validationCode":"const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());\nassert(payload.iss, 'iss required');\nassert(payload.aud === tokenEndpointUrl, 'aud must equal the Canvas OAuth2 token URL');\nassert(Date.now()/1000 - payload.iat < 3600, 'iat must be within 60 minutes');","typeGuard":"function claimsAreValid(token, expectedAud) {\n  try {\n    const p = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());\n    return p.iss && Array.isArray(p.aud) ? p.aud.includes(expectedAud) : p.aud === expectedAud && (Date.now()/1000 - p.iat) < 3600;\n  } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Set aud exactly to the Canvas token endpoint URL used in the request","Always include a current iss and fresh iat","Regenerate tokens instead of replaying cached ones across runs","Log the failing claim from the API message to speed diagnosis"],"tags":["lti","jwt","claims-validation","oauth2"],"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"}