{"record":{"id":"1efd7d9c010faf3d","repo":"instructure/canvas-lms","slug":"access-token-invalid-signature-likely-incorrect","errorCode":null,"errorMessage":"Access token invalid - signature likely incorrect","messagePattern":"Access token invalid - signature likely incorrect","errorType":"exception","errorClass":"AdvantageErrors::InvalidAccessToken","httpStatus":401,"severity":"error","filePath":"lib/lti/ims/advantage_access_token.rb","lineNumber":42,"sourceCode":"    # client_credentials flow such as NRPS and AGS\n    class AdvantageAccessToken\n      def initialize(raw_jwt_str)\n        @raw_jwt_str = raw_jwt_str\n      end\n\n      def validate!(expected_audience)\n        validate_claims!(expected_audience)\n        self\n      rescue Canvas::Security::InvalidToken => e\n        case e.cause\n        when JSON::JWT::InvalidFormat\n          raise AdvantageErrors::MalformedAccessToken, e\n        when JSON::JWS::UnexpectedAlgorithm\n          raise AdvantageErrors::InvalidAccessTokenSignatureType, e\n        when JSON::JWS::VerificationFailed\n          raise AdvantageErrors::InvalidAccessTokenSignature, e\n        else\n          raise AdvantageErrors::InvalidAccessToken.new(e, api_message: \"Access token invalid - signature likely incorrect\")\n        end\n      rescue JSON::JWT::Exception => e\n        raise AdvantageErrors::InvalidAccessToken, e\n      rescue Canvas::Security::TokenExpired => e\n        raise AdvantageErrors::InvalidAccessTokenClaims.new(e, api_message: \"Access token expired\")\n      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","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/lti/ims/advantage_access_token.rb#L24-L60","documentation":"Canvas's LTI Advantage access token validation (Lti::IMS::AdvantageAccessToken#validate!) decodes the submitted client_credentials JWT via Canvas::Security.decode_jwt. When decoding raises Canvas::Security::InvalidToken whose cause is not one of the specifically mapped JWS failures (bad format, unexpected algorithm, verification failed), the token is rejected as AdvantageErrors::InvalidAccessToken with the message 'Access token invalid - signature likely incorrect', since an unmapped decode failure almost always means the token was not signed with the developer key's private key.","triggerScenarios":"A POST to an LTI Advantage endpoint (NRPS/AGS) whose Authorization: Bearer JWT fails to verify but not with JSON::JWS::VerificationFailed exactly - e.g. tokens signed with the wrong key type, corrupted signature segments, missing/wrong kid, or tokens signed with a shared secret instead of the developer key RSA private key.","commonSituations":"Tool servers signing client_credentials tokens with the wrong or rotated key, using HS256 instead of RS256, deploying a new key pair without updating Canvas's public key (JWK) record, or truncating the JWT when constructing the Authorization header.","solutions":["Regenerate the client_credentials token signed with the RS256 private key matching the public JWK stored on the Canvas developer key","Verify the developer key's public JWK in Canvas matches the key pair currently used by the tool","Check the token is a well-formed three-segment JWT passed in the Authorization: Bearer header","Confirm no key rotation happened mid-flight; if it did, re-fetch Canvas's cached JWKs or restart the flow"],"exampleFix":"// before: signing with symmetric secret\nconst token = jwt.sign(payload, clientSecret, {algorithm: 'HS256'});\n// after: sign with the RSA private key registered on the developer key\nconst token = jwt.sign(payload, privateKeyPem, {algorithm: 'RS256', keyid: kid});","handlingStrategy":"try-catch","validationCode":"const parts = token.split('.');\nif (parts.length !== 3 || !parts[2]) throw new Error('malformed JWT');\nconst header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());\nif (header.alg !== 'RS256') throw new Error('Canvas expects RS256');","typeGuard":"function isRs256Jwt(token) {\n  const parts = token.split('.');\n  if (parts.length !== 3) return false;\n  try {\n    const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());\n    return header.alg === 'RS256' && !!header.kid;\n  } catch { return false; }\n}","tryCatchPattern":"begin\n  client.request_advantage_service(token)\nrescue Lti::IMS::AdvantageErrors::InvalidAccessToken => e\n  logger.warn(\"advantage token rejected: #{e.message}\")\n  refresh_signing_key_and_retry\nend","preventionTips":["Sign client_credentials tokens with RS256 using the private key matching the developer key's public JWK","Verify the developer key's public key/JWK is up to date in Canvas","Send the token as 'Authorization: Bearer <jwt>' without truncation","Test the signing flow after any key rotation or deploy"],"tags":["lti","jwt","signature","oauth2"],"backgroundTag":"jwt-signature-verification-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"}