instructure/canvas-lms · error · RuntimeError
can't build pseudonym_credentials except on just-generated…
Error message
can't build pseudonym_credentials except on just-generated token
What it means
SessionPersistenceToken#pseudonym_credentials builds a credential string that embeds the plaintext (uncrypted) session token, which only exists immediately after the token is generated. This guard fires when uncrypted_token is nil — i.e. the token was loaded from persistence and its plaintext value is no longer available, so credentials cannot be rebuilt. It prevents fabricating credentials from a token whose plaintext was never or no longer known.
Solutions
- Call pseudonym_credentials only on the freshly generated SessionPersistenceToken instance returned by the generator; store the resulting credential string immediately
- If you need new credentials later, generate a fresh persistence token rather than reusing the old one
- Inspect the call path: if it is invoked on a reloaded record, persist the previously built credential string instead
Example fix
// before token = SessionPersistenceToken.generate(pseudonym) creds = token.pseudonym_credentials token = SessionPersistenceToken.find(token.id) creds2 = token.pseudonym_credentials # raises // after token = SessionPersistenceToken.generate(pseudonym) creds = token.pseudonym_credentials # reuse `creds`; do not reload and call pseudonym_credentials again
Defensive patterns
Strategy: try-catch
Validate before calling
raise 'token not fresh' unless token.respond_to?(:uncrypted_token) && token.uncrypted_token
Type guard
token.try(:uncrypted_token).present?
Try / catch
begin
creds = token.pseudonym_credentials
rescue RuntimeError => e
regenerate_token if e.message.include?('just-generated token')
end Prevention
- Call pseudonym_credentials only on the just-generated object
- Store the credential string immediately after generation
- Never reload the record before extracting credentials
When it happens
Trigger: Calling pseudonym_credentials on a SessionPersistenceToken reloaded from the database (only crypted_token and salt persist); caching a persistence token object across requests and reusing it after the in-memory plaintext is gone; calling pseudonym_credentials twice, since the plaintext is single-use.
Common situations: Middleware that builds credentials once and later tries to rebuild them on a subsequent request; tests that construct SessionPersistenceToken records directly and expect credentials without generating a fresh token; session-store changes that reload the object.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot generate a symmetric, non-encrypted JWT
- Zip contents exceed course quota limit
- A new_id, '# ', referenced an existing # and the # with #…
- A new_integration_id, '#
- A student referenced a non-existent user #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/4b101e3aaa3dce3b.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/session_persistence_token.rb:90
return unless token.valid_token?(persistence_token, uuid)
token
end
def self.delete_expired(since)
where(updated_at: ...since.seconds.ago).in_batches(of: 10_000).delete_all
end
def valid_token?(persistence_token, uncrypted_token)
# if the pseudonym is marked deleted, the token can still be marked as
# valid, but the actual login step will fail as expected.
pseudonym &&
pseudonym.persistence_token == persistence_token &&
self.class.crypto.matches?(crypted_token, token_salt, uncrypted_token)
end
def pseudonym_credentials
raise "can't build pseudonym_credentials except on just-generated token" unless uncrypted_token
"#{id}::#{pseudonym.persistence_token}::#{uncrypted_token}"
end
def use!
destroy
pseudonym
end
end
View on GitHub (pinned to 1c9f0bb801)