{"record":{"id":"4b101e3aaa3dce3b","repo":"instructure/canvas-lms","slug":"can-t-build-pseudonym-credentials-except-on-just-generated","errorCode":null,"errorMessage":"can't build pseudonym_credentials except on just-generated token","messagePattern":"can't build pseudonym_credentials except on just-generated token","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"app/models/session_persistence_token.rb","lineNumber":90,"sourceCode":"    return unless token.valid_token?(persistence_token, uuid)\n\n    token\n  end\n\n  def self.delete_expired(since)\n    where(updated_at: ...since.seconds.ago).in_batches(of: 10_000).delete_all\n  end\n\n  def valid_token?(persistence_token, uncrypted_token)\n    # if the pseudonym is marked deleted, the token can still be marked as\n    # valid, but the actual login step will fail as expected.\n    pseudonym &&\n      pseudonym.persistence_token == persistence_token &&\n      self.class.crypto.matches?(crypted_token, token_salt, uncrypted_token)\n  end\n\n  def pseudonym_credentials\n    raise \"can't build pseudonym_credentials except on just-generated token\" unless uncrypted_token\n\n    \"#{id}::#{pseudonym.persistence_token}::#{uncrypted_token}\"\n  end\n\n  def use!\n    destroy\n    pseudonym\n  end\nend\n","sourceCodeStart":72,"sourceCodeEnd":100,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/session_persistence_token.rb#L72-L100","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n token = SessionPersistenceToken.generate(pseudonym)\n creds = token.pseudonym_credentials\n token = SessionPersistenceToken.find(token.id)\n creds2 = token.pseudonym_credentials # raises\n// after\n token = SessionPersistenceToken.generate(pseudonym)\n creds = token.pseudonym_credentials\n # reuse `creds`; do not reload and call pseudonym_credentials again\n","handlingStrategy":"try-catch","validationCode":"raise 'token not fresh' unless token.respond_to?(:uncrypted_token) && token.uncrypted_token","typeGuard":"token.try(:uncrypted_token).present?","tryCatchPattern":"begin\n  creds = token.pseudonym_credentials\nrescue RuntimeError => e\n  regenerate_token if e.message.include?('just-generated token')\nend","preventionTips":["Call pseudonym_credentials only on the just-generated object","Store the credential string immediately after generation","Never reload the record before extracting credentials"],"tags":["session","security","ruby"],"backgroundTag":"invalid-state-transition","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}