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

  1. Call pseudonym_credentials only on the freshly generated SessionPersistenceToken instance returned by the generator; store the resulting credential string immediately
  2. If you need new credentials later, generate a fresh persistence token rather than reusing the old one
  3. 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

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


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)