Repository navigation
Design and specification
Tip
TL;DR otphp for TOTP and HOTP, an own RFC 6287 implementation for OCRA, one encrypted table row per user, a Vue 3 frontend with three entry points.
twofactor_oath is a self-contained OATH one-time-password provider for Nextcloud. It implements TOTP (RFC 6238), HOTP (RFC 4226) and OCRA (RFC 6287), with per-token hash algorithm, length, period or counter, OCRA suite, and an optional predetermined secret. Tokens can be admin-managed and locked, provisioned in bulk, and imported from the bundled twofactor_totp. It targets hardware OATH tokens and elevated requirements, while twofactor_totp remains the simple self-service TOTP option.
How the app compares with the other Nextcloud second-factor apps is on Comparison with other providers.
A command-line provisioning command (occ) is intentionally not shipped: the bulk table and CSV paste import cover the interactive cases with live validation, and a headless command would add value only for scripted mass provisioning. It can be added later without schema changes.
flowchart TD
subgraph Frontend [Vue 3 frontend]
PS[Personal settings]
LS[Login setup]
AS[Admin settings]
end
CH[Login challenge template]
subgraph Backend [PHP backend]
SC[SettingsController]
AC[AdminController]
P[OtpProvider]
S[OtpService]
O[Ocra RFC 6287]
H[otphp TOTP/HOTP]
TI[TotpImporter]
CR[ICrypto]
DB[(twofactor_oath_secrets)]
end
PS -->|Axios| SC
LS -->|Axios| SC
AS -->|Axios| AC
CH --> P
SC --> S
AC --> S
AC --> TI
P --> S
S --> O
S --> H
S --> CR
S --> DB
TI --> DB
-
Backend: PHP and Nextcloud in the range
appinfo/info.xmldeclares. App idtwofactor_oath, namespaceOCA\TwoFactorOath, provider idoath, tabletwofactor_oath_secrets. -
Frontend: Vue 3 with the official
@nextcloud/vuev9 component library, Pinia,@chenfengyuan/vue-qrcode, built with Vite through@nextcloud/vite-config. Three entry points: personal settings, login setup, admin settings. -
Provider registration: declarative via
appinfo/info.xml(<two-factor-providers>), the standard for single-provider apps.
-
spomky-labs/otphp (MIT, de-facto standard) for TOTP and HOTP: object construction from a secret, custom digits, period and digest, provisioning URI with arbitrary parameters (the favicon
image=), and verification with window and leeway. No hand-rolled HOTP/TOTP crypto. - paragonie/constant_time_encoding (vendored by otphp) for Base32 secret generation.
- OCP\Security\ICrypto for encryption of secrets at rest (the Nextcloud instance key, AES-256-GCM).
-
Own OCRA implementation (see section 5): RFC 6287 is not covered by otphp, so it is implemented in
lib/Service/Ocra.php.
OCRA (RFC 6287) is the OATH challenge-response algorithm. otphp does not provide it, so the app ships a small, self-contained implementation in lib/Service/Ocra.php.
-
OCRA(K, DataInput) = Truncate(HMAC-H(K, DataInput))whereDataInput = OCRASuite | 0x00 | [C] | Q | [P] | [S] | [T]. - The challenge
Qis encoded for the numeric, alphanumeric and hex formats and padded to 128 bytes; truncation is the same dynamic truncation as HOTP. - Arbitrary-length numeric challenges are converted to bytes without
ext-bcmathorext-gmp(an in-class decimal to hex routine), and truncation stays exact even on 32-bit PHP, where the integer range is smaller (a 10-digit OTP's10^10modulus exceeds 2^31). -
parseSuite()parses the OCRASuite into hash, digit count, the data-input fields and the challenge format and length. It is also used by the UI and the validation path.
Correctness is verified against the official RFC 6287 Appendix C test vectors in tests/unit/Service/OcraTest.php (suite OCRA-1:HOTP-SHA1-6:QN08, the standard 20-byte key). The same engine powers the software OCRA token used for manual testing, so its output always matches the server.
A single table, twofactor_oath_secrets, one row per user. The schema carries every field the app needs from day one, so no follow-up migration is required.
| Column | Type | Meaning |
|---|---|---|
user_id |
string, unique | Owner |
type |
smallint | 1 TOTP, 2 HOTP, 3 OCRA |
secret |
text | Base32 secret, encrypted with ICrypto |
algorithm |
smallint | 1 SHA1, 2 SHA224, 3 SHA256, 4 SHA384, 5 SHA512 |
digits |
smallint | Code length (4 to 10) |
period |
smallint | TOTP step in seconds |
counter |
bigint | HOTP counter |
epoch |
bigint | TOTP T0 offset (RFC 6238); not exposed in the UI, kept for interop |
suite |
string, nullable | OCRA suite (OCRA only) |
state |
smallint | 1 created, 2 enabled |
locked |
boolean, nullable | Admin-managed: user cannot change or disable |
last_used |
bigint, nullable | Anti-replay: last accepted time slice (TOTP) or counter (HOTP) |
created_at |
bigint | Creation timestamp |
Integer codes are used internally (a single lib/Constants.php is the source of truth, mirrored in src/constants.js) rather than magic strings. Boolean columns are nullable because Nextcloud rejects a NOT NULL boolean.
stateDiagram-v2
[*] --> Disabled
Disabled --> Created: generate secret
Created --> Enabled: verify code or OCRA response
Created --> Disabled: cancel
Enabled --> Disabled: disable (fresh password)
Disabled is not persisted (no row means disabled). The controller and the frontend speak one 0/1/2 state machine. For OCRA, the Created to Enabled step is confirmed by a challenge-response rather than a code.
The advanced UI offers a Strict RFC compliance switch. It is a UI guard only: it greys out options the relevant RFC does not cover (for example SHA-224/384 for any OATH algorithm, or fewer than 6 digits for HOTP/TOTP), so the admin gets interoperable defaults. The backend always accepts the full range, so an already-provisioned token, or a value imported from CSV, keeps working even if it goes beyond the RFC. Responsibility for whether such a token works on a given device then lies with the operator. See Compatibility.
One component is written to be reusable in other Nextcloud projects:
-
SelectField.vue: a native<select>rendered in the exact visual language of@nextcloud/vue'sNcTextField(outlined box, box-shadow border, notched floating label). It measures its label and options and sizes itself to fit, down to a minimum width, so the notched label is never clipped in any language.
See Security model for encryption at rest, the forced password confirmation for revealing secrets or disabling the provider, anti-replay, and HOTP resynchronisation.
Getting started
Administration
Tokens
Security
Development