instructure/canvas-lms · error · InvalidTokenError

invalid aud

Error message

invalid aud

What it means

Lti::OAuth2::AccessToken#validate! verifies that the JWT's aud claim (treated as a list) includes the aud value the token was constructed/parsed with. If the audience does not match, InvalidTokenError 'invalid aud' is raised, preventing tokens minted for one audience from being replayed against another.

Solutions

  1. Pass the same audience URL to validate! that was used when the JWT was created
  2. Regenerate the token with the correct target audience via create_jwt
  3. Confirm you are not mixing Canvas environments/shards whose aud URLs differ

Example fix

# before
token = Lti::OAuth2::AccessToken.create_jwt(aud: 'https://other.canvas.test/api', sub: sub)
Lti::OAuth2::AccessToken.from_jwt(aud: 'https://canvas.prod/api', jwt: token.to_s).validate!
# after
token = Lti::OAuth2::AccessToken.create_jwt(aud: 'https://canvas.prod/api', sub: sub)
Lti::OAuth2::AccessToken.from_jwt(aud: 'https://canvas.prod/api', jwt: token.to_s).validate!
Defensive patterns

Strategy: validation

Validate before calling

aud_claim = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))['aud']
Array(aud_claim).include?(expected_aud) or raise 'aud mismatch'

Try / catch

begin
  token.validate!
rescue Lti::OAuth2::AccessToken::InvalidTokenError => e
  raise unless e.message == 'invalid aud'
  token = Lti::OAuth2::AccessToken.create_jwt(aud: target_aud, sub: sub)
  token.validate!
end

Prevention

When it happens

Trigger: Calling validate! with an aud argument that differs from the aud claim embedded in the JWT - e.g. using the token against a different service endpoint URL than the one it was issued for.

Common situations: Reusing a token across Canvas shards/accounts with different audience URLs, environment mismatches (test vs production Canvas domains), or the tool passing the wrong redirect/service URL as aud.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

Thrown at lib/lti/oauth2/access_token.rb:54

      rescue Canvas::Security::TokenExpired => e
        raise InvalidTokenError, "token has expired", e.backtrace
      rescue => e
        raise InvalidTokenError, e
      end

      def initialize(aud:, sub:, jwt: nil, reg_key: nil, shard_id: nil)
        @_jwt = jwt if jwt
        @reg_key = reg_key || (jwt && decoded_jwt["reg_key"])
        @aud = aud
        @sub = sub
        @shard_id = shard_id
      end

      def validate!
        decoded_jwt = Canvas::Security.decode_jwt(jwt)
        check_required_assertions(decoded_jwt.keys)
        raise InvalidTokenError, "invalid iss" if decoded_jwt["iss"] != ISS
        raise InvalidTokenError, "invalid aud" unless [*decoded_jwt[:aud]].include?(aud)
        raise InvalidTokenError, "iat must be in the past" unless Time.zone.at(decoded_jwt["iat"]) < Time.zone.now

        true
      rescue InvalidTokenError
        raise
      rescue Canvas::Security::TokenExpired => e
        raise InvalidTokenError, "token has expired", e.backtrace
      rescue => e
        raise InvalidTokenError, e
      end

      def to_s
        jwt
      end

      private

      def decoded_jwt

View on GitHub (pinned to 1c9f0bb801)