instructure/canvas-lms · error · InvalidTokenError
invalid iss
Error message
invalid iss
What it means
Lti::OAuth2::AccessToken#validate! checks that the JWT's iss claim equals the constant 'Canvas'. Tokens issued by Canvas always carry iss: 'Canvas'; a mismatch means the token was not minted by this code path or was tampered with, so InvalidTokenError 'invalid iss' is raised.
Solutions
- Ensure the token was produced by Canvas via Lti::OAuth2::AccessToken.create_jwt / the jwt builder with iss: 'Canvas'
- Inspect the token payload (base64-decode) and correct the iss claim to 'Canvas'
- If you meant to validate a tool-issued JWT, use the appropriate validator (e.g. AuthorizationValidator or AdvantageAccessToken) instead
Example fix
# before: custom issuer
body = {iss: 'my-tool', sub:, exp:, aud:, iat:, nbf:, jti:}
# after
body = {iss: Lti::OAuth2::AccessToken::ISS, sub:, exp:, aud:, iat:, nbf:, jti:} Defensive patterns
Strategy: validation
Validate before calling
iss = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))['iss']
raise 'not a Canvas-issued LTI token' unless iss == 'Canvas' Type guard
def canvas_issued_lti_jwt?(jwt)
payload = JSON.parse(Base64.urlsafe_decode64(jwt.split('.')[1]))
payload['iss'] == 'Canvas'
rescue JSON::ParserError, ArgumentError
false
end Prevention
- Only mint tokens through Lti::OAuth2::AccessToken.create_jwt or the jwt builder with iss: 'Canvas'
- Never feed tool-issued JWTs into this Canvas-internal validator
- Base64-decode the payload and check iss before calling validate! in tests
When it happens
Trigger: Calling validate! on a JWT whose iss claim is not exactly 'Canvas' - e.g. a token minted by another issuer, hand-crafted fixtures, or tokens built with a custom/expired iss value.
Common situations: Tool vendors passing their own OIDC launch JWTs into this Canvas-internal validator, upgrading Canvas code where ISS changed, or forging test tokens with issuer set to the tool's client_id.
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/b0314af4e910c69a.
Report an issue: GitHub.
Appendix: source
Thrown at lib/lti/oauth2/access_token.rb:53
new(aud:, sub: decoded_jwt[:sub], jwt:, shard_id: decoded_jwt[:shard_id])
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
View on GitHub (pinned to 1c9f0bb801)