156 lines
9.4 KiB
Markdown
156 lines
9.4 KiB
Markdown
# Second-factor service
|
|
|
|
Task [#40](https://git.sendico.io/tech/sxchnge/issues/40), branch `twofactor-40`.
|
|
|
|
## Ownership
|
|
|
|
`backend/product/twofactor` is a domain service inside the product executable.
|
|
It owns verified factors, preferred delivery channels, challenge snapshots,
|
|
issuance limits, code verification and single-use authorization proofs.
|
|
Its domain has no dependency on auth HTTP, JWTs or sessions. Shared Mongo
|
|
wrappers supply persistence and transactions; notifications only deliver messages.
|
|
|
|
Auth verifies the password, account and client, creates a pending JWT bound to the
|
|
client/device, and passes its hash as the login operation context. The service
|
|
selects the account's verified preferred factor and returns a masked challenge.
|
|
Auth calls `Complete` with the code and a callback that creates the session.
|
|
The service verifies the code, issues and consumes a proof, and commits the
|
|
callback's database changes in one transaction. Rejected guesses commit attempt
|
|
counters; callback failures roll back the code, proof and session together.
|
|
The HTTP response is written only after commit.
|
|
|
|
`Verify` and `Consume` also support separate domain steps for an operation that
|
|
needs a short-lived proof. Proofs bind the customer, purpose, operation and
|
|
challenge; they expire within two minutes and never outlive the challenge.
|
|
`Consume` checks the current profile and consumes the proof with the owner's
|
|
callback. Callbacks must only use this shared Mongo transaction: no external I/O,
|
|
response writes or irreversible side effects. Mongo may retry the callback.
|
|
An eventual remote service would need an explicit distributed completion protocol;
|
|
the callback contract cannot simply be translated into an RPC.
|
|
|
|
Signup email activation remains in `auth/flow`, with separate purpose and link
|
|
semantics. API-client secret login retains its existing separate behavior.
|
|
No generic public endpoint can choose an arbitrary authorization purpose.
|
|
|
|
## Sendico reuse
|
|
|
|
The token engine is the existing locally owned copy in
|
|
`backend/product/verification/store`. Its source is Sendico
|
|
`S_I/sendico` commit `8bb20dcd622dd8eb92d289b6b34411e7eccab083`, already recorded
|
|
in `backend/PROVENANCE.json` and `backend/PROVENANCE.md`. The new service uses
|
|
`NewOTPRequest`, `NewLinkRequest`, `CreateWithResult`, `Consume`, context binding,
|
|
idempotency, cooldown, max retries and issuance limits from that engine. It
|
|
retains SXCHNGE's subsequent random-key, empty-target-query and safe-log fixes.
|
|
No alternative OTP generator or token-consumption database was introduced.
|
|
|
|
The committed Sendico handlers inspected under
|
|
`api/edge/product/internal/server/verificationimp/` include `request.go`,
|
|
`verify.go`, `sendcode.go`, `target.go`, `service.go`, `types.go` and `token.go`.
|
|
They mix verification with session creation and private platform messaging and
|
|
allow recipient resolution unsuitable for a verified-factor boundary. Those
|
|
handlers were not copied. The orchestration, profile store, delivery interface
|
|
and HTTP settings adapter are new local code. There is no Sendico runtime,
|
|
private messaging or database dependency.
|
|
|
|
## Factors and policy
|
|
|
|
An active account's activated primary email is the initial verified factor.
|
|
Profiles are persisted in `two_factor_profiles`; challenge snapshots in
|
|
`two_factor_challenges` bind the chosen factor, locale, profile revision and
|
|
operation deadline. Snapshots cannot redirect a code during resend. Profile
|
|
changes invalidate outstanding challenges and proofs. A transactional profile
|
|
write serializes proof consumption against concurrent preference changes.
|
|
Challenge documents expire through a TTL index one day after their logical expiry;
|
|
logical expiry is checked synchronously and does not depend on TTL cleanup.
|
|
No existing customer migration is required: the initial profile is resolved
|
|
from the verified account and persisted on first completed verification.
|
|
|
|
Email is the installed channel driver. Users can enroll an additional email,
|
|
prove ownership and select it. Selection requires a fresh code at the current
|
|
preferred factor; enrollment alone cannot replace it. At most five factors
|
|
(including the primary email) are enrolled. Registered drivers form the supported
|
|
channel allow-list; unsupported or unavailable channels fail closed. There is no
|
|
automatic fallback, disable endpoint, factor removal or account recovery flow.
|
|
SMS, push, authenticator apps and passkeys require concrete implementations and
|
|
client support before use. The Flutter settings screen is still the existing
|
|
preview; these authenticated settings APIs are ready for a separate UI adapter.
|
|
|
|
Login, payout and preference scopes are recognized internally. Payout integration
|
|
is not added here; the payout owner must validate its immutable operation details
|
|
and call this service. An auth proof cannot authorize money operations.
|
|
|
|
## HTTP settings contract
|
|
|
|
All routes below are under `/api/v1/accounts` and require an existing active
|
|
account session through the standard `AccountHandler`. Pending login credentials
|
|
cannot access them. Account identity and operation scope come from the server,
|
|
not request fields. Responses use the existing `data` envelope and `no-store`.
|
|
|
|
| Method/path | Body | Result (202) |
|
|
| --- | --- | --- |
|
|
| GET `/two-factor` | none | `required: true`, `preferred`, `factors: [{id, channel, target}]` |
|
|
| POST `/two-factor/enroll` | `channel`, `address`, `idempotencyKey` | Challenge at the proposed address |
|
|
| POST `/two-factor/enroll/verify` | `challengeId`, `code` | Settings after ownership verification |
|
|
| POST `/two-factor/preference` | `factorId`, `idempotencyKey` | Challenge at the current preferred factor |
|
|
| POST `/two-factor/preference/verify` | `factorId`, `challengeId`, `code` | Settings after preference changes |
|
|
| POST `/two-factor/resend` | `challengeId`, `idempotencyKey` | Same settings operation and target, renewed code |
|
|
|
|
Challenge responses contain `challengeId`, `channel`, masked `target`, `expiresAt`
|
|
and `resendAt`. Login responses additionally include the pending JWT; login
|
|
resend/verify now require both `challengeId` and `pendingToken`. Codes, raw proofs
|
|
and full enrolled addresses are never returned in these responses.
|
|
|
|
Codes expire within ten minutes with five guesses per issued code. New issuance
|
|
uses a one-minute cooldown per challenge, plus shared fixed-window limits of five
|
|
per customer and twenty per requesting IP per hour across purposes. New challenges
|
|
cannot reset these delivery budgets. Same-key delivery retries recover the same
|
|
code, notification key and expiry; notification enqueueing deduplicates them.
|
|
A new accepted resend invalidates the prior code. A locked/expired operation must
|
|
restart. Replaying a used code or proof cannot execute the callback again.
|
|
|
|
## Rollout and recovery
|
|
|
|
Load revision 1 of `twofactor.code` alongside `auth.activation` and `auth.login`
|
|
from `backend/notifications/templates.example.json`; all have English/Russian
|
|
variants. Existing notification TLS/caller configuration and the private signing
|
|
key supply delivery and secret-derived issuance keys. No new service deployment
|
|
or network credentials are required. Mongo must support transactions (replica set).
|
|
The application creates the two new collections/index on startup.
|
|
|
|
Deploy backend and Flutter contract changes together. Earlier pending login codes
|
|
have no new challenge record and must restart password login after rollout.
|
|
Existing sessions and activation links are preserved. Key rotation invalidates
|
|
outstanding factor snapshots/proofs. Keep factors on an available driver when
|
|
changing configured drivers; there is no silent fallback.
|
|
|
|
A failed session write permits retrying the same code. A lost HTTP response after
|
|
a successful commit cannot replay the operation: restart login. Enrollment and
|
|
preference confirmation can be reconciled with GET settings after an ambiguous
|
|
response. Tokens and challenges commit before notification Send; a product crash
|
|
before Send still requires resend. No product delivery outbox or inbox receipts
|
|
are claimed. No live emails were sent during tests.
|
|
|
|
## Verification and observability
|
|
|
|
Real Mongo tests cover failed delivery replay, scope/customer isolation, expiry,
|
|
inactive accounts, attempts committed through outer transactions, proof replay,
|
|
callback rollback, concurrent completion, enrollment and preference invalidation.
|
|
The signup/login integration test exercises the new auth boundary. Transport
|
|
checks reject caller identity/purpose/destination overrides. Architecture tests
|
|
prevent the 2FA domain importing auth or sessions. Flutter adapter tests exercise
|
|
server challenge binding, resend recovery and unsupported channels.
|
|
|
|
Existing auth HTTP counters/timers and frontend journey events remain authoritative
|
|
for login outcomes. Domain logs record queued challenges, rejected codes and
|
|
completed operations with challenge IDs and purpose/channel, without codes,
|
|
proofs, keys or destinations. Prepared proof logs are debug-level and do not
|
|
represent a committed operation. See `docs/ux/auth-journeys.md` for attempt windows,
|
|
retry/abandonment rules and the existing production analytics export gap.
|
|
No new settings UI journey or production analytics sink is claimed.
|
|
|
|
The UI composition is unchanged. The shared DTO/mapper/service adapter preserves
|
|
masked delivery targets, provider error/retry handling and in-memory credential
|
|
storage. Existing widget tests cover mobile forms, keyboard behavior, loading,
|
|
cooldown and recovery; new adapter tests cover the changed backend contract.
|
|
Required completion checks and delivery are recorded in `docs/implementation.md`.
|