instructure/canvas-lms · error · AdvantageErrors::InvalidAccessTokenClaims
Access token expired
Error message
Access token expired
What it means
During AdvantageAccessToken#validate!, Canvas::Security.decode_jwt verifies the JWT exp claim and raises Canvas::Security::TokenExpired when it has lapsed. validate! translates this into AdvantageErrors::InvalidAccessTokenClaims with the API message 'Access token expired', indicating the token was structurally valid but used after its expiration time.
Solutions
- Request a fresh access token from the Canvas OAuth2 token endpoint and retry the call
- Shorten the client's token cache TTL to be safely below the exp lifetime (e.g. 50 minutes for a 1-hour token)
- Check NTP/clock synchronization on the tool server if tokens 'expire' immediately
Example fix
// before
const token = cachedToken;
await fetch(nrpsUrl, {headers: {Authorization: `Bearer ${token}`}});
// after
const token = isExpired(cachedToken) ? await fetchNewToken() : cachedToken;
await fetch(nrpsUrl, {headers: {Authorization: `Bearer ${token}`}}); Defensive patterns
Strategy: try-catch
Validate before calling
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
if (payload.exp * 1000 <= Date.now()) await refreshAccessToken(); Type guard
function tokenIsFresh(token, skewSec = 30) {
try {
const {exp} = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
return typeof exp === 'number' && exp - skewSec > Math.floor(Date.now() / 1000);
} catch { return false; }
} Try / catch
begin
call_nrps(token)
rescue Lti::IMS::AdvantageErrors::InvalidAccessTokenClaims => e
raise unless e.message.include?('expired')
token = fetch_new_token
retry
end Prevention
- Cache access tokens with a TTL safely below exp (e.g. 50 min for a 1-hour token)
- Check expiry before each request and refresh when within a small skew window
- Keep clocks NTP-synchronized
- Never reuse tokens across batch jobs longer than their lifetime
When it happens
Trigger: Any LTI Advantage service request (NRPS/AGS) where the client_credentials access token's exp claim is in the past at the time Canvas decodes it.
Common situations: Tools caching access tokens beyond their 1-hour lifetime, clock skew between tool server and Canvas, retrying with a stale cached token after a long job pause, or long-running batch jobs reusing one token.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- token has 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/f68fe52b20cf67dd.
Report an issue: GitHub.
Appendix: source
Thrown at lib/lti/ims/advantage_access_token.rb:47
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.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?View on GitHub (pinned to 1c9f0bb801)