FEAT-IDEN-101: Teacher TOTP Setup
[NEW - Compliant with DOM-IDEN Authority]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-101 | 1.3 | 2026-10-14 | 1.2 | Normative | NEW |
Important
v1.3 (2026-10-14), review correction. v1.2 named the algorithm (“scrypt via werkzeug”) at three places, which SPEC-SEC-001 §V.5 forbids: “Algorithm and parameters MUST be centralized in the canonical primitive, not duplicated in FEAT documents or route code.” Reworded to name only HASH_PASSWORD(), matching FEAT-IDEN-106’s phrasing. No behavior change.
v1.2 (2026-10-14), documentation modernization. §III.B.3 and the review checklist described backup-code hashing as “bcrypt + pepper.” The canonical primitive is HASH_PASSWORD() (salted, NOT peppered, per SPEC-SEC-001); sibling FEAT-IDEN-106 already used corrected wording. No behavior change.
I. Purpose
This FEAT activates TOTP (Time-based One-Time Password) two-factor authentication on a teacher User account during initial setup or reconfiguration. Per DOM-IDEN-003 §III.B:
“Teachers MUST enroll in TOTP (required 2FA) before gaining classroom access.”
This FEAT generates a TOTP secret, stores it encrypted on the User record, and provides QR code and backup codes for the teacher to secure.
Initial signup orchestration
Initial teacher signup is class-first at the workflow level: class metadata and
teacher display metadata are staged, then username and TOTP are collected. No
class, seat, or user row is durable before TOTP verification succeeds. The
final signup transaction creates the teacher User, ClassEconomy, teacher
Seat, and class-scoped IdentityProfile together. This FEAT’s existing-user
execution context applies to TOTP reconfiguration; it is not permission to
attach initial signup to an existing teacher account.
Initial staging is governed by INV-ARC-018’s temporary signup inventory. FEAT-IDEN-101 owns encrypted server staging and final initial provisioning. The browser carries only a random 256-bit signup nonce; its purpose-separated SHA-256 verifier is the staging key. Every stage checks the server’s fixed 30-minute expiry. Username changes replace the pending seed. Invalid TOTP may redisplay the same pending enrollment within that deadline. Restart deletes the previous attempt; hourly cleanup removes expired attempts. Completion locks the attempt, rechecks username/TOTP against its encrypted payload, creates the User, Class, Seat and profile, and deletes the attempt in one transaction. Failure rolls back provisioning and consumption. Replay cannot create another account. No existing-user or backup-code behavior is introduced by this initial-signup change.
Governing Authority:
- DOM-IDEN-003 §III.B (Teacher Authentication - TOTP Required)
- DOM-IDEN-003 §IV (Teacher Recovery with Student-Verified Codes)
- DOM-IDEN-005 §VIII (Identity Binding)
- FEAT-CORE-000 (Feature Execution Constitutional Directive)
II. Execution Context
1. Required Inputs
user_id: The teacherUserbeing onboarded (from session or context).seat_id: The adminSeatin the target class (from context).class_id: The class context (from session or context).idempotency_key: Client-provided unique request ID for retry safety.
2. Resolved Context (MANDATORY)
Before mutation, the FEAT MUST resolve:
- The
Userrecord matchinguser_idin theuserstable. - The
Seatrecord matchingseat_idin theseatstable. - Verify that
User.id == Seat.user_id(seat is bound to this user). - Verify that
Seat.role = 'teacher'(seat is a teacher seat). - Verify that
Seat.class_id == class_id(seat is in the correct class).
III. Orchestration Logic
A. Verification Phase (Read-Only)
Step 1: Validate Teacher User State
- Query
Userrecord whereid = user_id. - Verify that
user_role = 'teacher'. - Failure Behavior: Abort with
INVALID_USER_ROLEif user is not a teacher.
Step 2: Check TOTP Enrollment Status
- Query
User.totp_secret_encrypted. - If already enrolled (not NULL), return
ALREADY_ENROLLED(idempotent success). - If not enrolled (NULL), proceed to generate secret.
Step 3: Validate Seat State
- Query
Seatrecord whereSeat.id = seat_id. - Verify that
Seat.user_id = user_id(seat is bound to the target user). - Verify that
Seat.role = 'teacher'(seat is a teacher seat). - Failure Behavior: Abort with
INVALID_SEAT_STATEif seat is not a teacher seat.
B. Mutation Phase (Atomic Transaction)
All mutations in this section MUST occur within a single database transaction.
Step 1: Generate TOTP Secret
Perform secret generation outside the transaction:
secret = pyotp.random_base32()(generates 32-character base32-encoded secret).encrypted_secret = NORMALIZE_TOTP_FOR_STORAGE(secret)(applies AES-128 encryption withENCRYPTION_KEY).
Note: The unencrypted secret is displayed to the teacher ONLY during this initial setup. It is NOT stored in plaintext.
Step 2: Activate TOTP on User
Update the User record:
- Set
totp_secret_encrypted = encrypted_secret(store encrypted TOTP seed).
Per DOM-IDEN-003 §III.B:
“Teacher-specific fields:
totp_secret_encrypted”
Step 3: Generate and Persist Backup Codes
Generate one-time backup codes for account recovery:
- Create 10 backup codes (alphanumeric, format: XXXX-XXXX-XXXX-XXXX).
- Hash each backup code with
HASH_PASSWORD()(canonical credential primitive fromSPEC-SEC-001, same as passwords — seeFEAT-IDEN-106). - Persist hashes to
User.backup_codes_encrypted(encrypted JSON array of hashed codes). - For display, keep plaintext codes in memory (display once, then discard from memory).
Backup Code Lifecycle:
- Codes are stored as one-way
HASH_PASSWORD()hashes (cannot be decrypted, only verified) - When a code is used (e.g., during account recovery), it is validated against stored hash
- Used codes are marked with
used_attimestamp and cannot be reused - All codes are invalidated when TOTP secret is reset (FEAT-IDEN-106)
- Old backup codes are replaced with new ones during TOTP updates
Note: Backup codes are distinct from student-verified recovery codes (FEAT-IDEN-104). These are for manual fallback only.
Step 4: Audit Trace
Per FEAT-CORE-000 §III.4:
- Call
DOM-OPSto emit anACT-IDEN-101audit event. - Required fields:
feat_id: “FEAT-IDEN-101”user_id: The teacheruser_idseat_id: The adminseat_idclass_id: The classroom contextidempotency_key: The provided keyoutcome:"TOTP_ENROLLED"(only possible outcome for new enrollment)timestamp: ISO 8601 UTC timestamp
Note: DO NOT log the unencrypted secret or backup codes.
IV. Invariants & Constraints
1. TOTP Required for Teachers (MANDATORY)
Per DOM-IDEN-003 §III.B:
“All teacher users MUST have TOTP enrolled before they can authenticate.”
Once this FEAT completes, the teacher’s login flow requires TOTP code verification (orchestrated by a separate authentication FEAT, T-001).
2. Secret Encryption (MANDATORY)
The TOTP secret SHALL be encrypted at rest using NORMALIZE_TOTP_FOR_STORAGE() with ENCRYPTION_KEY from environment.
3. Single Enrollment (MANDATORY)
Once enrolled, this FEAT SHALL NOT re-enroll (idempotent). Changing TOTP secret requires FEAT-IDEN-106 (Update TOTP Secret), which invalidates the old secret.
4. Atomic Transaction (MANDATORY)
All mutations SHALL occur in a single transaction. If any step fails, complete rollback occurs.
5. One-Time Display (MANDATORY)
The unencrypted secret is displayed to the teacher ONLY ONCE during initial setup via QR code and text representation. After display, the secret is never revealed again (only the encrypted version is stored).
6. One-Time Backup Code Usage (MANDATORY)
Each backup code can be used exactly once during account recovery. Once a code is used (validated and marked with used_at timestamp), it cannot be reused. This is tracked at validation time (not during storage). Validation SHALL:
- Retrieve stored hash from
User.backup_codes_encrypted - Verify plaintext code against hash using
verify_password() - Check that
used_at IS NULL(code has not been used) - On successful validation, set
used_at = NOW()to mark as consumed
V. Idempotency
Mechanism: The combination of idempotency_key and user_id acts as the idempotency lock.
Behavior:
- If a retry occurs with the same
idempotency_key:- Check if
User.totp_secret_encrypted IS NOT NULL(TOTP already enrolled). - If true, return success with outcome
ALREADY_ENROLLED(no duplicate state). - If false, re-attempt the full mutation.
- Check if
- Replayed requests with the same
idempotency_keySHALL NOT create duplicate audit events.
Client Responsibility:
- Generate a stable
idempotency_key(e.g., based on session or user context). - Retry on transient errors with the same key.
- Server stores key on audit log to detect and skip replays.
VI. Failure Scenarios
When the FEAT fails, the system SHALL:
- Rollback all mutations atomically.
- Emit audit event with outcome
FAILEDand error code. - Return error response to client.
- NOT advance the teacher past the TOTP setup step.
Failure Cases:
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| User not a teacher | INVALID_USER_ROLE |
403 | “Only teachers can enroll TOTP.” |
| Invalid seat | INVALID_SEAT_STATE |
409 | “Invalid seat. Please start over.” |
| Already enrolled | ALREADY_ENROLLED |
200 | “TOTP is already enrolled on this account. To change it, use the Update TOTP option.” |
| Encryption error | ENCRYPTION_FAILED |
500 | “Failed to encrypt TOTP secret. Please try again.” |
| Database error | INTERNAL_ERROR |
500 | “An error occurred during setup. Please try again.” |
VII. Audit Requirements
The DOM-OPS audit log MUST contain:
| Field | Type | Required | Rationale |
|---|---|---|---|
feat_id |
String | ✓ | Identifies the FEAT (always “FEAT-IDEN-101”) |
user_id |
Integer | ✓ | The teacher user |
seat_id |
Integer | ✓ | The admin seat |
class_id |
UUID | ✓ | The classroom context |
idempotency_key |
String | ✓ | Replay detection |
outcome |
Enum | ✓ | Must be: TOTP_ENROLLED or ALREADY_ENROLLED |
timestamp |
ISO 8601 | ✓ | UTC timestamp of enrollment |
totp_secret |
String | ✗ | DO NOT log (security) |
backup_codes |
Array | ✗ | DO NOT log (security) |
error_code |
String | ⚠️ | Only if outcome is FAILED |
Outcomes:
TOTP_ENROLLED: Successful TOTP enrollment on a previously-uncredentialed teacher.ALREADY_ENROLLED: Idempotent return; TOTP was already enrolled (no new secret generated).
VIII. Display Requirements
After successful enrollment, the route handler SHALL display to the teacher:
-
QR Code: Scannable image representing the TOTP secret.
- Generated from
secretusingqrcode.QRCode()or similar. - Encodes:
otpauth://totp/classroom-economy:{user_id}@{class_id}?secret={secret}&issuer=ClassroomEconomy.
- Generated from
-
Text Representation: Raw base32 secret for manual entry.
- Format:
{secret}(32-character string). - Display once, then hide (user must write down or save).
- Format:
-
Backup Codes: 10 single-use recovery codes.
- Format: XXXX-XXXX-XXXX-XXXX.
- Display once, then hide.
- Instruct teacher to store securely.
-
Setup Confirmation: Button to confirm teacher has saved codes.
- Clicking confirms teacher has secured the secret and backup codes.
- Cannot proceed without confirmation.
IX. Integration with FEAT-IDEN-104 (Student-Verified Recovery)
TOTP setup enables the recovery mechanism:
TOTP Setup (FEAT-IDEN-101)
├─ Teacher enrolls TOTP secret
└─ Teacher secures backup codes
Teacher Recovery Flow (FEAT-IDEN-103 through FEAT-IDEN-105)
├─ If teacher loses access, initiate recovery (FEAT-IDEN-103)
├─ Students provide recovery codes (FEAT-IDEN-104)
└─ Teacher identity verified, TOTP can be reset (FEAT-IDEN-106)Both TOTP enrollment and recovery use the same recovery_requests and student_recovery_codes tables. Recovery is triggered separately but assumes TOTP is already enrolled.
X. Dependencies
docs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.mddocs/DOMAIN/DOM-IDEN-003_TEACHER_IDENTITY_ARCHITECTURE.mddocs/DOMAIN/DOM-IDEN-005_IDENTITY_BINDING_AND_LIFECYCLE.mddocs/DOMAIN/DOM-IDEN-006_CANONICAL_CONTEXT_RESOLUTION.md
XI. Implementation Checklist
Before code review, verify:
- User role validation ensures only teachers can enroll
- TOTP secret is generated with
pyotp.random_base32() - Secret is encrypted with
NORMALIZE_TOTP_FOR_STORAGE()before storage - Seat binding is verified before setup
- QR code generated correctly for authenticator apps
- Backup codes are hashed with
HASH_PASSWORD()(perSPEC-SEC-001, same as passwords) - Unencrypted secret displayed only once
- All mutations occur in a single transaction
- Audit event emitted with all required fields
- Rollback occurs on any failure
- Secret and backup codes are NOT logged in audit
- Idempotency check prevents duplicate enrollment
- Tests cover successful enrollment and idempotent retry
- Tests cover already-enrolled scenario
XII. Amendments
Revisions to this document SHALL:
- Increment the version.
- Update the effective date.
- Maintain consistency with DOM-IDEN-003 §III.B.
- Maintain consistency with FEAT-CORE-000.
- Maintain consistency with FEAT-IDEN-103, FEAT-IDEN-104, FEAT-IDEN-106.
Version 1.1 adds encrypted, nonce-bound initial signup staging (2026-09-16).
Last updated: 2026-10-14