Tiger_Model_UserCredential

@api Β· extends Tiger_Model_Table

UserCredential β€” durable authentication factors (1-to-many with user).

Each row is one way a user can prove who they are: a password, a verified SMS number, a TOTP secret, a passkey, an SSO link. See migration 0004 for the schema and the per-type secret semantics.

This model owns the factor mechanics (hashing, verification, touch/verify state). Higher-level orchestration (the login flow, session issuance) belongs in an auth SERVICE, not here β€” the model stays a data gateway with factor-aware helpers.

Methods

hashPassword()

hashPassword($plain)

Hash a plaintext password. PASSWORD_DEFAULT = bcrypt today, upgraded by PHP over time. A password hash is one-way, so it's stored as-is (not encrypted).

  • $plain string β€” the plaintext password

Returns string β€” the (peppered) password hash

setPassword()

setPassword($userId, $plain)

Set (create or replace) the user's single password credential. Idempotent β€” one password row per user (enforced by the UNIQUE key with identifier='').

  • $userId string β€” the owning user's id
  • $plain string β€” the new plaintext password

Returns string β€” credential_id

verifyPassword()

verifyPassword($userId, $plain)

Verify a plaintext password against the user's stored hash. Records last_used_at on success. Returns false if there's no password credential.

  • $userId string β€” the owning user's id
  • $plain string β€” the candidate plaintext password

Returns bool β€” true if the password matches

addSms()

addSms($userId, $e164)

Add an (unverified) SMS factor β€” a phone number for OTP/2FA. verified_at stays NULL until confirmed via an auth_challenge round-trip (then markVerified()).

  • $userId string
  • $e164 string β€” phone in E.164 form, e.g. +15551234567

Returns string β€” credential_id

factor()

factor($userId, $type, $identifier = '')

A specific factor for a user. Singleton types (password/totp) use the default identifier ''; sms/oauth pass the phone/subject.

  • $userId string β€” the owning user's id
  • $type string β€” factor type (TYPE_PASSWORD, TYPE_SMS, …)
  • $identifier string β€” the factor sub-key (phone/subject; '' for singletons)

Returns Zend_Db_Table_Row_Abstract|null

factorsFor()

factorsFor($userId)

All (non-deleted) factors a user holds β€” e.g. for a "security settings" screen.

  • $userId string β€” the owning user's id

Returns Zend_Db_Table_Rowset_Abstract β€” the user's factor rows

findVerifiedByIdentifier()

findVerifiedByIdentifier($type, $identifier)

Reverse lookup: which credential owns this identifier for a type? Powers "log in by phone" (sms) and SSO callback resolution (oauth). Only matches VERIFIED factors β€” an unconfirmed phone can't be used to authenticate.

  • $type string β€” factor type (TYPE_SMS, TYPE_OAUTH, …)
  • $identifier string β€” the factor identifier (phone/subject) to look up

Returns Zend_Db_Table_Row_Abstract|null

markVerified()

markVerified($credentialId)

Mark a factor confirmed (e.g. after a successful SMS OTP).

  • $credentialId string β€” the credential_id to confirm

Returns int β€” affected rows

touch()

touch($credentialId)

Record that a factor was just used to authenticate.

  • $credentialId string β€” the credential_id to stamp last_used_at on

Returns int β€” affected rows

passwordCredential()

passwordCredential($userId)

The user's password credential row, or null.

  • $userId string β€” the owning user's id

Returns Zend_Db_Table_Row_Abstract|null

isLockedOut()

isLockedOut($credential)

Is this credential currently locked out (too many recent failures)?

  • $credential Zend_Db_Table_Row_Abstract|null β€” the credential row to check

Returns bool β€” true if the credential is locked out right now

recordFailure()

recordFailure($credentialId)

Record a failed authentication; lock the credential after MAX_FAILURES.

  • $credentialId string β€” the credential_id that failed

recordSuccess()

recordSuccess($credentialId)

Record a successful authentication: clear the failure counter + lockout, touch.

  • $credentialId string β€” the credential_id that succeeded

activeTotp()

activeTotp($userId)

The user's active, confirmed TOTP factor row, or null.

  • $userId string β€” the owning user's id

Returns Zend_Db_Table_Row_Abstract|null

hasActiveTotp()

hasActiveTotp($userId)

Does the user have a confirmed authenticator-app factor?

  • $userId string β€” the owning user's id

Returns bool β€” true if a confirmed TOTP factor exists

replaceTotp()

replaceTotp($userId, $encryptedSecret, array $recoveryHashes)

Enable (or re-enroll) TOTP: replace any prior TOTP + recovery rows with a fresh confirmed secret and a new set of hashed backup codes, atomically-ish (one owner of these rows). Call only AFTER the enrollment code has been verified.

  • $userId string
  • $encryptedSecret string β€” Tiger_Crypto::encrypt() of the base32 secret
  • $recoveryHashes array β€” sha256 hashes of the plaintext backup codes

removeTotp()

removeTotp($userId)

Disable TOTP entirely: drop the secret and all remaining backup codes.

  • $userId string β€” the owning user's id

redeemRecoveryCode()

redeemRecoveryCode($userId, $plainCode)

Redeem a single-use recovery code (constant-time match against remaining codes). On success the code is consumed (deleted) so it can't be reused. Returns true iff a code matched.

  • $userId string β€” the owning user's id
  • $plainCode string β€” the recovery code the user entered

Returns bool β€” true if a code matched and was consumed

rekeyTotpSecrets()

rekeyTotpSecrets()

Re-encrypt every stored TOTP secret under the CURRENT crypto key (decrypting via current-or-retired). The eager half of key rotation: run after crypto:rotate-key, then the retired key can be dropped. Skips rows that fail to decrypt (already unrecoverable) and reports the counts.

Returns array{rekeyed:int,failed:int}

recoveryCount()

recoveryCount($userId)

How many unused recovery codes the user has left (for the security screen).

  • $userId string β€” the owning user's id

Returns int β€” the number of remaining recovery codes