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
- Pass the same audience URL to validate! that was used when the JWT was created
- Regenerate the token with the correct target audience via create_jwt
- 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
- Pass the identical aud to create_jwt/from_jwt and validate!
- Avoid sharing tokens across Canvas environments, shards, or accounts
- Derive aud from the target service URL rather than hardcoding it
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
- Access token expired
- Access token invalid - signature likely incorrect
- either the tool proxy or developer key were not found
- iat must be in the past
- Invalid access token field/s: #
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_jwtView on GitHub (pinned to 1c9f0bb801)