instructure/canvas-lms · error · AdvantageErrors::InvalidAccessTokenClaims
Invalid access token field/s: #
Error message
Invalid access token field/s: #{validator.error_message} What it means
AdvantageAccessToken#validate_claims! runs the decoded JWT through Canvas::Security::JwtValidator with require_iss, a 60-minute max iat age, an expected audience, and jti check skipped. If any claim fails validation, the validator's error_message (considered safe to expose) is raised verbatim as AdvantageErrors::InvalidAccessTokenClaims with 'Invalid access token field/s: ...'.
Solutions
- Read the specific field named in the message and correct that claim in the token request
- Set aud exactly to the Canvas OAuth2 token endpoint URL used in the request
- Ensure iat is current (within 60 minutes) and iss is present and correct
- Regenerate the token rather than replaying a cached one
Example fix
// before: wrong audience
const payload = {iss: clientId, sub: clientId, aud: 'https://canvas.example.com', iat: oldIat, exp};
// after
const payload = {iss: clientId, sub: clientId, aud: 'https://canvas.example.com/login/oauth2/token', iat: Math.floor(Date.now()/1000), exp: Math.floor(Date.now()/1000)+3600}; Defensive patterns
Strategy: validation
Validate before calling
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
assert(payload.iss, 'iss required');
assert(payload.aud === tokenEndpointUrl, 'aud must equal the Canvas OAuth2 token URL');
assert(Date.now()/1000 - payload.iat < 3600, 'iat must be within 60 minutes'); Type guard
function claimsAreValid(token, expectedAud) {
try {
const p = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
return p.iss && Array.isArray(p.aud) ? p.aud.includes(expectedAud) : p.aud === expectedAud && (Date.now()/1000 - p.iat) < 3600;
} catch { return false; }
} Prevention
- Set aud exactly to the Canvas token endpoint URL used in the request
- Always include a current iss and fresh iat
- Regenerate tokens instead of replaying cached ones across runs
- Log the failing claim from the API message to speed diagnosis
When it happens
Trigger: Client_credentials tokens posted to NRPS/AGS endpoints with a wrong aud (not the Canvas OAuth2 token URL), missing iss, an iat older than 60 minutes, or otherwise malformed claims.
Common situations: Using the wrong audience URL (e.g. account-scoped vs canvas.instructure.com), reusing a token generated more than an hour ago, tool SDKs omitting iss, or pointing tools at a different Canvas host than the one minting tokens.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 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 aud
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/54856a4e458517c5.
Report an issue: GitHub.
Appendix: source
Thrown at lib/lti/ims/advantage_access_token.rb:66
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?
raise AdvantageErrors::InvalidAccessTokenClaims.new(
nil,
api_message: "Invalid access token field/s: #{validator.error_message}"
)
end
end
def claim(name)
decoded_jwt[name]
end
def decoded_jwt
@_decoded_jwt = Canvas::Security.decode_jwt(@raw_jwt_str)
end
def client_id
claim("sub")
end
endView on GitHub (pinned to 1c9f0bb801)