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

Invalid access token field/s: #

Error message

Invalid access token field/s: #{validator.error_message}

What it means

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

Solutions

  1. Read the specific field named in the message and correct that claim in the token request
  2. Set aud exactly to the Canvas OAuth2 token endpoint URL used in the request
  3. Ensure iat is current (within 60 minutes) and iss is present and correct
  4. Regenerate the token rather than replaying a cached one

Example fix

// before: wrong audience
const payload = {iss: clientId, sub: clientId, aud: 'https://canvas.example.com', iat: oldIat, exp};
// after
const 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};
Defensive patterns

Strategy: validation

Validate before calling

const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
assert(payload.iss, 'iss required');
assert(payload.aud === tokenEndpointUrl, 'aud must equal the Canvas OAuth2 token URL');
assert(Date.now()/1000 - payload.iat < 3600, 'iat must be within 60 minutes');

Type guard

function claimsAreValid(token, expectedAud) {
  try {
    const p = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
    return p.iss && Array.isArray(p.aud) ? p.aud.includes(expectedAud) : p.aud === expectedAud && (Date.now()/1000 - p.iat) < 3600;
  } catch { return false; }
}

Prevention

When it happens

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

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

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

      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
        )

        # In this case we know the error message can just be safely shunted into the API response (in other cases
        # we're more wary about leaking impl details)
        unless validator.valid?
          raise AdvantageErrors::InvalidAccessTokenClaims.new(
            nil,
            api_message: "Invalid access token field/s: #{validator.error_message}"
          )
        end
      end

      def claim(name)
        decoded_jwt[name]
      end

      def decoded_jwt
        @_decoded_jwt = Canvas::Security.decode_jwt(@raw_jwt_str)
      end

      def client_id
        claim("sub")
      end
    end

View on GitHub (pinned to 1c9f0bb801)