Skip to content

[compliance] Backlog — MFA: document the extension point + ship reference plugins (TOTP / WebAuthn) #78

Description

@perki

Tracked in the compliance matrix as MFA-MODERN-METHODS.


Backlog — MFA: document the extension point + ship reference plugins (TOTP / WebAuthn)

Filed: 2026-05-19 (from the compliance-matrix implementer-perspective
gap-probing session — Q4 on authentication strength).
Related: sits in the perspective of the OAuth2 + signed-request +
app-account modernisation work (issue #36). The MFA-methods question is
the second factor's modernisation alongside that work's modernisation of
the first factor + bearer-token model.

What's already shipped

MFA is pluggable. An abstract base class
components/business/src/mfa/Service.ts defines challenge() and
verify() methods that subclasses override. Two subclasses ship
today:

  • ChallengeVerifyService — two-step external provider
    (one HTTP endpoint sends the challenge, another verifies the
    response).
  • SingleService — one-step external provider (single HTTP
    endpoint does both).

Both currently target SMS providers by convention — the
mfaConfig shape under services.mfa carries challenge / verify
URL templates with {{ username }} placeholder substitution. But
the abstraction itself is generic over HTTP-callable providers.

An operator can today plug in any HTTP-based MFA provider that
fits the challenge/verify or single-step shape
by editing
services.mfa config — without writing code. That includes:

  • TOTP-as-a-service providers (Twilio Authy, Auth0 MFA API).
  • Push providers exposing HTTP webhooks (Duo, Authy push).
  • Any HMAC-signed-OTP webhook service.

An operator writing a new subclass can extend Service to plug in
any provider — internal or external.

What's missing

Three real gaps:

1. Documentation

docs/pryv-primitives.md MFA section currently says
"SMS-based by default; opt-in per services.mfa.mode". That
phrasing reads as "Pryv supports SMS-MFA only", which is wrong —
it reads as a feature scope when it's actually a default. Auditors

  • implementers asking "is your MFA AAL2-compatible?" need to see
    the extension point + the supported plugin shapes.

Doc gaps to fill:

  • Update docs/pryv-primitives.md MFA primitive description to
    call out the extension point, not just the SMS default.
  • Add a "writing an MFA provider" page to dev-site (config-driven
    HTTP provider, then code-level Service subclass).
  • Examples in dev-site for at least: a TOTP service config (using
    e.g. Twilio Verify or an internal TOTP server), and an external
    push provider config.
  • A statement of which AAL the shipped subclasses can achieve when
    configured with which providers (e.g., Authy push → AAL2; SMS-only
    via Twilio → AAL1 effectively under NIST AAL guidance).

2. Reference plugin: TOTP (RFC 6238) server-side

The current Service shape assumes the provider lives outside
Pryv (HTTP roundtrip). A TOTP implementation can be HTTP-based
(point at an external Twilio Verify endpoint), but it's also
common to want server-side TOTP where Pryv stores the secret +
verifies the code without an external dependency.

For server-side TOTP, an operator needs to write a Service
subclass that:

  • Stores the user's TOTP secret on a system-stream at enrollment.
  • On verify(), reads the secret + runs RFC 6238 TOTP verification
    against the submitted code.
  • Generates recovery codes at enrollment (single-use, stored
    encrypted at rest).

A reference TOTP subclass shipped in components/business/src/mfa/
would let operators enable strong AAL2 MFA without a third-party
service dependency.

3. Reference plugin / abstraction: WebAuthn (W3C Level 3)

WebAuthn does not fit the current Service shape because it's
not a simple HTTP challenge/verify — it requires:

  • Server-issued CredentialCreationOptions (random challenge +
    RP info) at enrollment.
  • Server verification of AuthenticatorAttestationResponse on enroll
    (signature verification against attestation statement).
  • Server-issued challenge + verification of
    AuthenticatorAssertionResponse on each login.
  • Public-key + signature-counter storage per credential per user.

This is a substantively different abstraction. Two paths:

  • Path A — broaden the Service base class to handle
    non-HTTP-roundtrip ceremonies (challenge/verify methods that
    return / accept structured data rather than just calling HTTP
    endpoints). Existing SMS subclasses stay.
  • Path B — add a sibling abstraction LocalService for
    ceremonies that happen in-process (WebAuthn, server-side TOTP).
    Keep Service for HTTP-provider passthrough.

Either path is roughly the same amount of code. Path B is cleaner
because the two patterns (external HTTP provider vs in-process
ceremony) really are different beasts; merging them into one base
class would force every subclass to handle both cases.

Once the abstraction is in place, a WebAuthnService reference
implementation ships. Storage: WebAuthn credential record per user
(credential ID, public key, signature counter, transports) on the
MFA system-stream.

Constraints

  • Must coexist with the SMS implementations already shipped.
    Operator config services.mfa.allowedMethods (or the operator
    picking a single service via services.mfa.providerType)
    determines which is active. Multi-method per user is a stretch
    goal.
  • Recovery flow: TOTP + WebAuthn need recovery-code provisioning at
    enrollment, stored encrypted at rest, single-use.
  • MFA test series (MA01-MA09) extends with per-method tests
    (MA10+ for TOTP, MA20+ for WebAuthn, etc.).
  • WebAuthn requires HTTPS in production (already satisfied by Pryv's
    default TLS posture).

Relationship to the OAuth2 work

The OAuth2 work (issue #36) modernises the authorization layer (RFC 6749 OAuth2,
app accounts, signed requests, operator-side revocation).

This MFA work is orthogonal but adjacent: it modernises the
authentication layer (strong primary + second factor).

That work's side notes already include "3rd party SSO" — both fit the
same modernisation arc. A vanilla RFC 6749 client wants both a real
OAuth2 flow and a real second factor. A modern HIPAA / ISO /
DiGA-compliant deployment needs both.

Sequencing options:

  1. Doc-only fix first (no code change; lifts the matrix posture by
    making the extension point visible). Hours of work.
  2. Plus reference TOTP subclass (no abstraction change; days).
  3. Plus WebAuthn (needs the LocalService abstraction; weeks).

Sequencing 1 → 2 → 3 lets each step ship independently while the OAuth2 work
proceeds on its own track.

Status

Not started. Backlog.

Matrix impact when shipped

Scope Row Today After step 1 (docs) After step 3 (WebAuthn)
hipaa-security 164.312(d) Impl | High unchanged; caveat removed unchanged; tests expanded
iso-27001 A.8.5 Impl | High unchanged unchanged; per-method tests
iso-27001 A.5.17 F: Infra | Med unchanged F: Infra | High
diga A1.2.4 Impl | High unchanged unchanged
hipaa-security 164.308(a)(5)(ii)(D) Configurable | Med Configurable | High unchanged

The current Implemented | High rows are defensible against regulator
text (HIPAA / ISO / DiGA don't prescribe specific factors). Step 1
(documentation) removes the ambiguity that makes the rows look weak
under auditor scrutiny.

Related

  • The OAuth2 / app-accounts work: [V2] App Accounts / Oauth2 & signatures #36
  • Compliance-matrix proposal mirror:
    compliance-matrix/proposals/mfa-modern-methods.md.
  • MFA code (open-pryv.io): components/business/src/mfa/Service.ts
    • subclasses ChallengeVerifyService.ts + SingleService.ts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    compliance-matrixTracked from compliance-matrix (pryv/compliance-matrix)

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions