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
- Regenerate the client_credentials token signed with the RS256 private key matching the public JWK stored on the Canvas developer key
- Verify the developer key's public JWK in Canvas matches the key pair currently used by the tool
- Check the token is a well-formed three-segment JWT passed in the Authorization: Bearer header
- 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
- Sign client_credentials tokens with RS256 using the private key matching the developer key's public JWK
- Verify the developer key's public key/JWK is up to date in Canvas
- Send the token as 'Authorization: Bearer <jwt>' without truncation
- Test the signing flow after any key rotation or deploy
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access token expired
- either the tool proxy or developer key were not found
- iat must be in the past
- Invalid access token field/s: #
- invalid aud
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.minutesView on GitHub (pinned to 1c9f0bb801)