FEAT-IDEN-106: Teacher Update TOTP Secret
[NEW - Compliant with DOM-IDEN Authority]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-106 | 1.1 | 2026-09-15 | 1.0 | Normative | NEW |
I. Purpose
This FEAT allows a teacher to change their TOTP (Time-based One-Time Password) secret. This is needed in two scenarios:
- Account Recovery: After successful student-verified recovery (FEAT-IDEN-105), teacher must reset TOTP.
- Credential Rotation: Teacher proactively changes TOTP for security (e.g., lost authenticator device).
Per DOM-IDEN-003 §III.B:
“Teachers can update their TOTP secret at any time. The old secret is invalidated immediately upon update.”
This FEAT generates a new TOTP secret, encrypts and stores it, and optionally generates new backup codes.
Governing Authority:
- DOM-IDEN-003 §III.B (Teacher Authentication - TOTP Update)
- DOM-IDEN-005 §VIII (Identity Binding)
- FEAT-CORE-000 (Feature Execution Constitutional Directive)
Recovery versus authenticated rotation
For recovery, the dedicated recovery completion contract at the end of this document specifies the full input, authorization, credential and completion behavior. The Seat-context, current-enrollment, backup-code and optional confirmation-checkbox steps below describe authenticated rotation only; they do not add recovery gates. Recovery requires verification of the newly enrolled TOTP before replacing credentials.
II. Execution Context
1. Required Inputs
user_id: The teacherUserupdating TOTP (from session, authenticated).seat_id: The adminSeatin the target class (from context).class_id: The class context (from session or context).recovery_context_id: Optional recovery request ID if this is part of account recovery (FEAT-IDEN-105).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: Validate Seat State
- Query
Seatrecord whereid = seat_id. - Verify that
user_id = user_id(seat is bound to the target user). - Verify that
role = 'teacher'(seat is a teacher seat). - Failure Behavior: Abort with
INVALID_SEAT_STATEif seat is not a teacher seat.
Step 3: Verify Recovery Context (If Provided)
- If
recovery_context_idis provided:- Query
recovery_requestswhereid = recovery_context_id. - Verify that
user_id = user_id(recovery is for this teacher). - Require pending status and the live server-stored setup nonce verifier from FEAT-IDEN-105.
- Require
completed_at IS NULL; completed recovery cannot authorize another reset. - Require the original five-day
expires_atdeadline; no additional post-completion recovery window exists.
- Query
- Failure Behavior: Abort with
RECOVERY_CONTEXT_INVALIDif recovery context is invalid or the original recovery deadline has elapsed.
Note: If no recovery context, this is a proactive TOTP rotation (not part of recovery).
Step 4: Check Current TOTP
- Query
User.totp_secret_encrypted. - Verify that TOTP is already enrolled (not NULL).
- Failure Behavior: Abort with
TOTP_NOT_ENROLLEDif no TOTP exists (use FEAT-IDEN-101 for initial setup).
B. Mutation Phase (Atomic Transaction)
All mutations in this section MUST occur within a single database transaction.
Step 1: Generate New TOTP Secret
Perform secret generation outside the transaction:
new_secret = pyotp.random_base32()(generates 32-character base32-encoded secret).encrypted_secret = NORMALIZE_TOTP_FOR_STORAGE(new_secret)(applies AES-128 encryption withENCRYPTION_KEY).
Note: The unencrypted new secret is displayed to the teacher ONLY ONCE. It is NOT stored in plaintext.
Step 2: Replace TOTP on User
Update the User record:
- Set
totp_secret_encrypted = encrypted_secret(store encrypted new TOTP seed, overwriting old secret).
Per DOM-IDEN-003 §III.B:
“Updating the TOTP secret invalidates the previous secret immediately.”
Important: The old secret is overwritten, not preserved. There is no rollback to old credentials after update.
Step 3: Generate and Persist New Backup Codes
Generate and persist fresh one-time backup codes (replaces old codes):
- Create 10 new backup codes (alphanumeric, format: XXXX-XXXX-XXXX-XXXX).
- Hash each backup code with
HASH_PASSWORD()(canonical credential primitive from SPEC-SEC-001). - Persist hashes to
User.backup_codes_encrypted(encrypted JSON array of hashed codes, replacing old codes). - For display, keep plaintext codes in memory (display once, then discard from memory).
Backup Code Lifecycle During Update:
- All old backup codes are immediately invalidated and replaced
- New codes are stored as canonical one-way password verifiers
- Each code can be used once; usage is tracked by
used_attimestamp - After use, code cannot be reused
- If teacher loses both new secret AND all backup codes, they must initiate recovery again
Note: Old backup codes are invalidated immediately and cannot be recovered. New codes must be secured by the teacher.
Step 4: Clear Recovery Context (If Applicable)
If recovery_context_id was provided:
- Atomically set status
verified, completed_at, and clear all setup and saved-progress material with credential replacement and passkey revocation. - Clear the recovery session and require fresh login; a completed request cannot authorize another reset.
Step 5: Audit Trace
Per FEAT-CORE-000 §III.4:
- Call
DOM-OPSto emit anACT-IDEN-106audit event. - Required fields:
feat_id: “FEAT-IDEN-106”user_id: The teacheruser_idseat_id: The adminseat_idclass_id: The classroom contextrecovery_context_id: If part of recovery, the recovery request ID (optional)idempotency_key: The provided keyoutcome:"TOTP_UPDATED"(only possible outcome)timestamp: ISO 8601 UTC timestamp
Note: DO NOT log the old or new unencrypted secrets or backup codes.
IV. Invariants & Constraints
1. Old Secret Invalidated (MANDATORY)
Per DOM-IDEN-003 §III.B:
“Updating TOTP immediately invalidates the old secret. There is no grace period or rollback.”
Once the new secret is stored, the old secret cannot be used to generate valid TOTP codes.
2. TOTP Must Already Be Enrolled (MANDATORY)
This FEAT only updates existing TOTP. For initial enrollment, use FEAT-IDEN-101.
3. One-Time Display (MANDATORY)
The unencrypted new secret is displayed to the teacher ONLY ONCE during update. After display, it is never revealed again.
4. Atomic Transaction (MANDATORY)
All mutations SHALL occur in a single transaction. If any step fails, complete rollback occurs and old secret remains valid.
5. New Backup Codes (MANDATORY)
When updating TOTP, new backup codes must be generated. Old backup codes become invalid.
6. Recovery Context Optional (MANDATORY)
TOTP update can occur:
- As part of account recovery (with
recovery_context_id), OR - As proactive credential rotation (without recovery context).
Both scenarios use the same FEAT logic.
7. One-Time Backup Code Usage (MANDATORY)
Backup codes are replaced entirely during TOTP update. Old codes are invalidated. Each new code can be used exactly once during future recovery. Validation SHALL:
- Retrieve stored hash from updated
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 - Codes cannot be reused after
used_atis set
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
recovery_requests.status = 'verified'(recovery already complete) OR the secret has been updated recently. - If true, return success with outcome
ALREADY_UPDATED(no duplicate state). - If false, re-attempt the full mutation.
- Check if
- Replayed requests with the same
idempotency_keySHALL NOT create duplicate TOTP resets or backup code generations.
Client Responsibility:
- Generate a stable
idempotency_key(e.g., based on session). - Retry on transient errors with the same key.
- Server stores key on audit log to detect and skip replays.
Important: Once a TOTP secret is updated, there is no “previous” secret to restore. Idempotency means returning success on retry with the same key, not rolling back to an old secret.
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.
- Keep old TOTP secret intact (no partial updates).
Failure Cases:
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| User not a teacher | INVALID_USER_ROLE |
403 | “Only teachers can update TOTP.” |
| Invalid seat | INVALID_SEAT_STATE |
409 | “Invalid seat. Please start over.” |
| TOTP not enrolled | TOTP_NOT_ENROLLED |
409 | “TOTP is not yet enrolled. Use setup to enable TOTP first.” |
| Recovery context invalid | RECOVERY_CONTEXT_INVALID |
409 | “Recovery context is invalid. Please start a new recovery.” |
| Encryption error | ENCRYPTION_FAILED |
500 | “Failed to encrypt new TOTP secret. Please try again.” |
| Database error | INTERNAL_ERROR |
500 | “An error occurred during update. 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-106”) |
user_id |
Integer | ✓ | The teacher user |
seat_id |
Integer | ✓ | The admin seat |
class_id |
UUID | ✓ | The classroom context |
recovery_context_id |
Integer | ⚠️ | Recovery request ID if part of recovery, else NULL |
idempotency_key |
String | ✓ | Replay detection |
outcome |
Enum | ✓ | Must be: TOTP_UPDATED or ALREADY_UPDATED |
timestamp |
ISO 8601 | ✓ | UTC timestamp of update |
old_secret |
String | ✗ | DO NOT log (security) |
new_secret |
String | ✗ | DO NOT log (security) |
backup_codes |
Array | ✗ | DO NOT log (security) |
error_code |
String | ⚠️ | Only if outcome is FAILED |
Outcomes:
TOTP_UPDATED: Successful TOTP secret update.ALREADY_UPDATED: Idempotent return; TOTP already updated in a previous request.
VIII. Display Requirements
After successful TOTP update, the route handler SHALL display to the teacher:
-
Confirmation: “TOTP secret updated successfully!”
-
New QR Code: Scannable image for the new secret.
- Generated from
new_secretusingqrcode.QRCode(). - Encodes:
otpauth://totp/classroom-economy:{user_id}@{class_id}?secret={new_secret}&issuer=ClassroomEconomy.
- Generated from
-
Text Representation: Raw base32 new secret for manual entry.
- Format:
{new_secret}(32-character string). - Display once, then hide.
- Format:
-
New Backup Codes: 10 single-use recovery codes.
- Format: XXXX-XXXX-XXXX-XXXX.
- Display once, then hide.
- Instruct teacher to store securely (replacing old codes).
-
Setup Confirmation: Button to confirm teacher has saved new codes.
- Clicking confirms teacher has secured the new secret and backup codes.
- Cannot proceed without confirmation (if part of recovery).
-
If Recovery Context: “Your account access has been restored. Your TOTP is now updated.”
IX. Integration with FEAT-IDEN-105 (Recovery)
TOTP update completes the account recovery flow:
FEAT-IDEN-103: Teacher Initiates Recovery
└─ Creates recovery_request
FEAT-IDEN-104: Students Generate Recovery Codes
└─ Populates selected code rows created by FEAT-IDEN-103
FEAT-IDEN-105: Teacher Validates Recovery Codes
└─ Verifies all-or-nothing, authorizes one server-bound setup session
FEAT-IDEN-106: Update TOTP Secret (THIS FEAT)
├─ Teacher resets TOTP with new secret
└─ Completes recovery (marks status: verified)After FEAT-IDEN-106 completes with recovery context, the teacher has full account access restored.
X. Security Considerations
Old Secret Invalidation
The moment the new secret is stored, the old secret becomes invalid. There is NO grace period. If the teacher needs to revert, they must restart account recovery.
Backup Code Replacement
Old backup codes are invalidated. The teacher MUST save the new backup codes. If they lose both the new secret AND all backup codes, they must initiate account recovery again.
Rate Limiting (Recommended)
To prevent brute-force TOTP changes, rate-limit TOTP updates:
- Max 5 updates per day per user (or similar threshold).
- Log all updates for anomaly detection.
XI. Dependencies
docs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.mddocs/FEATURE-EXECUTION/FEAT-IDEN-101_TEACHER_TOTP_SETUP.mddocs/FEATURE-EXECUTION/FEAT-IDEN-105_TEACHER_RECOVERY_CODE_VALIDATION.mddocs/DOMAIN/DOM-IDEN-003_TEACHER_IDENTITY_ARCHITECTURE.mddocs/DOMAIN/DOM-IDEN-005_IDENTITY_BINDING_AND_LIFECYCLE.md
XII. Implementation Checklist
Before code review, verify:
- User role validation ensures only teachers can update
- Current TOTP verification (TOTP already enrolled)
- New secret generation with
pyotp.random_base32() - New secret encrypted with
NORMALIZE_TOTP_FOR_STORAGE() - Old secret completely replaced (no fallback)
- Seat binding is verified before update
- Recovery context validation (if provided)
- QR code generated correctly for new secret
- New backup codes generated and hashed
- Old backup codes invalidated (implicitly, by not storing separately)
- All mutations occur in a single transaction
- RecoveryRequest marked as completed (if recovery context)
- Audit event emitted with all required fields
- Rollback occurs on any failure
- Secrets and backup codes are NOT logged
- Idempotency check prevents duplicate updates
- Tests cover successful update and idempotent retry
- Tests cover recovery context scenario
- Tests cover proactive rotation scenario (no recovery context)
XIII. 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-101 (initial TOTP setup).
This is version 1.0 of FEAT-IDEN-106 (new specification, 2026-08-09).
Recovery completion contract
For recovery, resolve the principal exclusively from the request. Class/Seat input requirements above apply to authenticated rotation only. Lock the teacher and request, verify pending/unexpired state, complete current class-confirmation coverage and constant-time comparison with the server nonce verifier. Read the encrypted pending seed and username from the server; verify the new authenticator code before any credential change. Atomically update username fields through the canonical builder, replace the TOTP seed, delete all user-owned passkeys, rotate the login-session nonce, mark verified/completed_at, and erase setup/saved material. No legacy User.salt or username field exists. A retry, revoked nonce, expiry or changed coverage fails closed. Username uniqueness conflicts roll back all credential and passkey changes. This recovery path does not generate backup codes or open a post-completion window.
Last updated: 2026-09-15