instructure/canvas-lms · error · AdvantageErrors::InvalidAccessToken

Access token invalid - signature likely incorrect

Error message

Access token invalid - signature likely incorrect

What it means

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.

Solutions

  1. Regenerate the client_credentials token signed with the RS256 private key matching the public JWK stored on the Canvas developer key
  2. Verify the developer key's public JWK in Canvas matches the key pair currently used by the tool
  3. Check the token is a well-formed three-segment JWT passed in the Authorization: Bearer header
  4. Confirm no key rotation happened mid-flight; if it did, re-fetch Canvas's cached JWKs or restart the flow

Example fix

// before: signing with symmetric secret
const token = jwt.sign(payload, clientSecret, {algorithm: 'HS256'});
// after: sign with the RSA private key registered on the developer key
const token = jwt.sign(payload, privateKeyPem, {algorithm: 'RS256', keyid: kid});
Defensive patterns

Strategy: try-catch

Validate before calling

const parts = token.split('.');
if (parts.length !== 3 || !parts[2]) throw new Error('malformed JWT');
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
if (header.alg !== 'RS256') throw new Error('Canvas expects RS256');

Type guard

function isRs256Jwt(token) {
  const parts = token.split('.');
  if (parts.length !== 3) return false;
  try {
    const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
    return header.alg === 'RS256' && !!header.kid;
  } catch { return false; }
}

Try / catch

begin
  client.request_advantage_service(token)
rescue Lti::IMS::AdvantageErrors::InvalidAccessToken => e
  logger.warn("advantage token rejected: #{e.message}")
  refresh_signing_key_and_retry
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/1efd7d9c010faf3d. Report an issue: GitHub.

Appendix: source

Thrown at lib/lti/ims/advantage_access_token.rb:42

    # client_credentials flow such as NRPS and AGS
    class AdvantageAccessToken
      def initialize(raw_jwt_str)
        @raw_jwt_str = raw_jwt_str
      end

      def validate!(expected_audience)
        validate_claims!(expected_audience)
        self
      rescue Canvas::Security::InvalidToken => e
        case e.cause
        when JSON::JWT::InvalidFormat
          raise AdvantageErrors::MalformedAccessToken, e
        when JSON::JWS::UnexpectedAlgorithm
          raise AdvantageErrors::InvalidAccessTokenSignatureType, e
        when JSON::JWS::VerificationFailed
          raise AdvantageErrors::InvalidAccessTokenSignature, e
        else
          raise AdvantageErrors::InvalidAccessToken.new(e, api_message: "Access token invalid - signature likely incorrect")
        end
      rescue JSON::JWT::Exception => e
        raise AdvantageErrors::InvalidAccessToken, e
      rescue Canvas::Security::TokenExpired => e
        raise AdvantageErrors::InvalidAccessTokenClaims.new(e, api_message: "Access token expired")
      rescue AdvantageErrors::AdvantageServiceError
        raise
      rescue => e
        raise AdvantageErrors::AdvantageServiceError, e
      end

      def validate_claims!(expected_audience)
        validator = Canvas::Security::JwtValidator.new(
          jwt: decoded_jwt,
          expected_aud: expected_audience,
          require_iss: true,
          skip_jti_check: true,
          max_iat_age: 60.minutes

View on GitHub (pinned to 1c9f0bb801)