struct

KemalIdentity::MFA::Factor

Inherits Struct < Value < Object

One enrolled second factor.

The parameters travel with the factor

digits, period and algorithm are stored per row rather than read from configuration at verification time. An application that later moves from six digits to eight, or from SHA-1 to SHA-256, would otherwise break every already-enrolled authenticator at once — the app on the phone keeps computing what it was given at enrolment, and nothing tells it otherwise. Stored per row, a default change applies to new enrolments and leaves existing ones working.

Constructors

new(id : String, account_id : String, sealed_secret : Bytes, created_at : Time, label : String = "authenticator", kind : FactorKind = FactorKind::TOTP, digits : Int32 = TOTP::DEFAULT_DIGITS, period : Time::Span = TOTP::DEFAULT_PERIOD, algorithm : TOTP::Algorithm = TOTP::Algorithm::SHA1, confirmed_at : Time | Nil = nil, last_used_counter : Int64 | Nil = nil, consecutive_failures : Int32 = 0, last_failure_at : Time | Nil = nil, disabled_at : Time | Nil = nil)
Source

Instance methods

account_id
Source
algorithm
Source
confirmed?

Whether enrolment finished.

Source
confirmed_at

When a code from this factor first verified, or nil while enrolment is unfinished.

Enrolment is two steps on purpose. A secret that was generated but never proved is a secret nobody may actually hold: a mis-scanned QR code, a clock two minutes out, an app that silently failed to save. Treating it as a factor immediately is how a person locks themselves out of their own account, so an unconfirmed factor never authenticates and never counts towards "this account has MFA".

Source
consecutive_failures

How many times in a row a code has been offered for this factor and been wrong.

Per factor rather than per account, because that is the thing NIST SP 800-63B bounds: "the verifier SHALL limit consecutive failed authentication attempts using a specific authenticator on a single subscriber account to no more than 100 by disabling that authenticator." A rate limiter cannot answer this — it is keyed by account, it is usually a window that resets, and in this shard's default it lives in one process's memory. So the count belongs on the row, which is also where django-otp keeps it (throttling_failure_count).

Reset to zero by a successful verification, not by the passage of time. "Consecutive" means since the last success.

Source
created_at
Source
digits
Source
disabled?

Whether this factor was disabled for consecutive failures.

Source
disabled_at

When this factor was disabled for having failed too many times in a row, or nil.

A disabled factor never authenticates and never counts towards "this account has MFA", exactly like an unconfirmed one — but it is a different state and stays visible in a management listing, because the person needs to be told which device stopped working and why. Re-enabling is deliberately not an operation on this contract: what a deployment does about it — support ticket, re-enrolment, an unlock e-mail — is policy, and the two available answers are already there (Service#remove then a fresh enrolment).

Source
inspect(io : IO) : Nil

Redacted. sealed_secret is ciphertext rather than a secret, and printing it in a crash report would still be handing an attacker half of what they need.

Source
kind
Source
label

What the person calls it, for a list of "your second factors". Never a secret.

Source
last_failure_at

When the most recent wrong code was offered for this factor, or nil if never.

The other half of django-otp's pair: the count alone cannot tell an operator whether a factor is failing right now or failed months ago, and a deployment that wants its own per-factor delay curve computes it from this.

Source
last_used_counter

The highest TOTP counter this factor has been used at, or nil if never.

The replay defence. Without it a code stays usable for its whole window plus the drift either side, which is precisely the window an attacker who watched someone type it is working in.

Source
period
Source
sealed_secret

The TOTP shared secret, encrypted. See SecretBox for why this one is not a digest.

Source
to_s(io : IO) : Nil

Redacted. sealed_secret is ciphertext rather than a secret, and printing it in a crash report would still be handing an attacker half of what they need.

Source
usable?

Whether a code from this factor may authenticate anything: enrolment finished and it has not been disabled. This is what the verification path and Service#enrolled? ask.

Source