FEAT-IDEN-004: Student Recovery Code Acceptance & Session Authorization
[Session-bound recovery under DOM-IDEN-002 §IX]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-004 | 1.4 | 2026-10-01 | 1.3 | Normative | ACTIVE |
I. Purpose
This FEAT is the second step of the student account recovery workflow. A student who has lost access to their account submits a reset code (generated by a teacher via FEAT-IDEN-003), and the system consumes the code and authorizes only the accepting session to replace credentials. Existing credentials remain intact until setup succeeds.
Per DOM-IDEN-002 §IX (Student Account Recovery) Steps 2-3:
Step 2: “Student submits reset code. Backend validates… Code has not been used, Code has not expired.” Step 3: “Student proceeds through the standard credential setup flow… No identity verification fields (name, DOB, or any PII) are re-entered. All three credentials are fully replaced atomically. Old credentials become invalid immediately.”
This FEAT implements Steps 2-3 of the recovery flow.
Governing Authority:
- DOM-IDEN-002 §IX (Student Account Recovery)
- DOM-IDEN-005 §IX (Identity Recovery)
- FEAT-CORE-000 (Feature Execution Constitutional Directive)
II. Execution Context
0. Precondition: No Authenticated Principal (MANDATORY)
Per DOM-IDEN-005 §VII (v2.3, operator ruling 2026-10-01), recovery, which serves someone who cannot sign in, requires the absence of an
authenticated canonical principal. If the request boundary has authenticated a user_id for the
session (DOM-IDEN-006 §VIII step 1) — student, teacher or sysadmin, with or without a selected
class — the request is refused before this FEAT runs: no Turnstile check, no reset-code lookup or consumption, no lock, and no
setup state written. The refusal infers nothing about how the signed-in principal relates to the
identity being established. One gate enforces this for every endpoint of the workflow
(app/routes/identity_establishment.py); a structural test fails any view that reaches
establishment state without being declared there.
1. Required Inputs
reset_code: The 8-character alphanumeric code provided by the teacher.idempotency_key: Client-provided unique request ID.
Note: No PII is required. Per DOM-IDEN-002 §IX: “No identity verification fields (name, DOB, or any PII) are re-entered.”
2. Resolved Context (MANDATORY)
Before mutation, the FEAT MUST resolve:
student_user: TheUserrecord matching the reset code.
No seat, class, IdentityProfile, or participation context may be resolved. Teacher issuance resolves the selected Seat to its User in FEAT-IDEN-003; student redemption and subsequent credential replacement operate on that User alone (DOM-IDEN-002 §IX).
III. Orchestration Logic
A. Verification Phase (Read-Only)
Step 1: Validate Reset Code Input
- Verify
reset_codeis provided and non-empty. - Normalize: Strip whitespace, convert to uppercase.
- Failure Behavior: Abort with
INVALID_CODE_FORMATif format is clearly wrong.
Step 2: Find User by Reset Code
- Query
Userwherereset_code = reset_code. - Security Constraint: Use generic failure message regardless of outcome (don’t reveal if user exists).
- Failure Behavior: Abort with
INVALID_OR_EXPIRED_CODE(does not distinguish “code not found” vs “code expired”).
Step 3: Validate Code State
- Verify
User.reset_code IS NOT NULL(code is present). - Verify
User.reset_code_expires_at IS NOT NULL(expiry is set). - Verify
NOW() < reset_code_expires_at(code has not expired). - Verify code has not been cleared (would indicate it was already used).
- Failure Behavior: Abort with
INVALID_OR_EXPIRED_CODE(generic for security).
Step 4: Principal-Only Recovery Boundary
Do not query or select a Seat, class, or IdentityProfile. Recovery authorization comes from the code on the student User; no active or claimed seat is required at redemption. Do not derive a classroom actor from that User.
Step 5: Lock and Revalidate the User
Lock the matching User row for update. Require exactly one match and recheck the code and strict expiry under the lock. Concurrent acceptance has one winner; subsequent attempts fail generically. Hold the lock through the following commit.
B. Mutation Phase (Atomic Transaction)
Step 1: Create Session Authority
Generate a fresh cryptographically random recovery-session nonce. Persist only
its verifier in User.recovery_setup_nonce_hash and the original code deadline
in User.recovery_setup_expires_at, using SPEC-SEC-001 §V.4. Never log either.
Step 2: Consume the Code
Clear reset_code, reset_code_generated_at, and reset_code_expires_at in the
same transaction as creating the server-side recovery-session record. Do not
clear or replace credentials or the authenticated-session nonce here. If the
transaction fails, neither consumption nor recovery authorization commits.
Step 3: Preserve Participation
Do not read or mutate seats, bindings, profiles, claim artifacts, or class state.
Step 4: Clear Session State (Application-Level)
The route handler SHALL clear recovery-specific session variables:
- Clear
recovery_student_ref - Clear any other recovery state
Note: This is application-level housekeeping, not a database mutation.
Step 5: Initialize Credential Setup Session State
The route handler SHALL prepare the credential setup session:
- Clear
onboarding_seat_refand stale generated-username/theme state - Set
onboarding_user_ref = user_id(for FEAT-IDEN-002) - Set
recovery_setup_authorizationto the newly generated nonce and use a non-permanent browser session. The User reference alone never authorizes setup. - Every setup request must check the nonce against the User’s current verifier and strict expiry. Session-cookie validity alone is insufficient.
Note: Set session authority only after the database transaction commits. A lost response cannot restore the consumed code; a new teacher-issued code is required.
Step 6: Emit Audit Trace
Per FEAT-CORE-000 §III.4:
- Call
DOM-OPSto emitACT-IDEN-004audit event. - Required fields:
feat_id: “FEAT-IDEN-004”user_id: The recovereduser_ididempotency_key: The provided keyoutcome:"RECOVERY_SESSION_AUTHORIZED"(only possible outcome)timestamp: ISO 8601 UTC timestamp
Security Note: DO NOT log the reset code. Log only that acceptance succeeded; never log session authorization material.
IV. Invariants & Constraints
1. Single-Use Reset Code (MANDATORY)
Per DOM-IDEN-002 §IX:
“Reset codes are single-use — consumed when accepted, before credential setup.”
- Code is consumed atomically with session authorization on acceptance.
- Attempting to reuse the same code returns
INVALID_OR_EXPIRED_CODE.
2. Hard 10-Minute TTL (MANDATORY)
Per DOM-IDEN-002 §IX:
“Hard TTL: 10 minutes.”
- Code must not be used after
reset_code_expires_at. - No exceptions, no renewals, no sliding window.
3. Atomic Session Authorization (MANDATORY)
- Code consumption and server-side nonce creation commit together.
- Credentials remain intact until FEAT-IDEN-002 successfully replaces them.
- Completion consumes the nonce atomically with credentials and session rotation.
- Lost/expired sessions require a new teacher-issued code. No cross-session replay.
4. No PII Requirement (MANDATORY)
Per DOM-IDEN-002 §IX Step 3:
“No identity verification fields (name, DOB, or any PII) are re-entered.”
This FEAT:
- ✓ Accepts only the reset code
- ❌ Does NOT ask for name, email, DOB, or any PII
- ❌ Does NOT ask for student ID or roster matching
- ❌ Does NOT require answering security questions
5. Generic Error Messages (MANDATORY)
Per DOM-IDEN-002 §IX:
“On failure: return a generic failure message. Do not reveal whether a specific identity exists.”
Always respond with:
- Valid Code: “Recovery code verified. Please set up your new username and credentials.”
- Invalid/Expired Code: “Invalid or expired recovery code.” (regardless of reason)
6. Rate Limiting (MANDATORY)
Per DOM-IDEN-002 §IX:
“Rate-limit… reset code… submission.”
Recommended limits:
- Per IP address: Max 5 attempts per 15 minutes
- Per reset code: Max 10 total attempts (then it expires or gets locked)
- Lockout: After 3 failed attempts for a code, add 5-minute wait before next attempt
Exceeding limits returns 429 Too Many Requests.
7. Idempotency (MANDATORY)
- Idempotency key + reset code ensures replay safety.
- Retrying a consumed code fails generically, even with the same request key.
- Audit correlation does not grant or replay recovery authority.
- Audit log uses key to detect duplicate events.
V. Failure Scenarios
When the FEAT fails, the system SHALL:
- Rollback all mutations (code and session authority remain unchanged).
- Emit audit event with outcome
FAILEDand error code. - Return generic error (don’t reveal whether user exists).
- NOT advance to credential setup.
Failure Cases:
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| Code not provided | INVALID_CODE_FORMAT |
400 | “Invalid or expired recovery code.” |
| Code not found | CODE_NOT_FOUND |
400 | “Invalid or expired recovery code.” |
| Code expired (TTL exceeded) | CODE_EXPIRED |
400 | “Invalid or expired recovery code.” |
| Code already used | CODE_ALREADY_USED |
400 | “Invalid or expired recovery code.” |
| Rate limit exceeded | RATE_LIMIT_EXCEEDED |
429 | “Too many attempts. Please wait before trying again.” |
| Database error | INTERNAL_ERROR |
500 | “An error occurred. Please try again.” |
| Browser is signed in (an authenticated principal is present) | SIGNED_IN |
409 (a page load renders the refusal with 200) | “We are having trouble determining who you are right now. For your protection, this request was cancelled.” The page offers Return to login, which ends the current sign-in through that role’s logout. |
VI. Audit Requirements
The DOM-OPS audit log MUST contain:
| Field | Type | Required | Rationale |
|---|---|---|---|
feat_id |
String | ✓ | Identifies FEAT (always “FEAT-IDEN-004”) |
user_id |
Integer | ✓ | The recovered user |
idempotency_key |
String | ✓ | Replay detection |
outcome |
Enum | ✓ | RECOVERY_SESSION_AUTHORIZED or FAILED |
timestamp |
ISO 8601 | ✓ | UTC timestamp of validation |
reset_code |
String | ✗ | DO NOT log (security) |
error_code |
String | ⚠️ | Only if outcome is FAILED |
Outcomes (Only One Possible):
RECOVERY_SESSION_AUTHORIZED: Code consumed, server-backed session authorized, existing credentials preserved.
VII. Integration with Recovery Flow
FEAT-IDEN-004 is Step 2 of the student recovery workflow:
Step 1: FEAT-IDEN-003 (Teacher Initiates - completed)
└─ Reset code generated, waiting for student
Step 2: FEAT-IDEN-004 (Student Validates Code) ← YOU ARE HERE
├─ Student action: Enter code provided by teacher
├─ System: Consume code and create server-backed session nonce
├─ Old credentials: Preserved until replacement commits
└─ Output: Session state prepared for credential setup
Step 3: FEAT-IDEN-002 (Credential Setup - reused)
├─ Student action: Enter new username, PIN, passphrase
├─ System: Hash and activate new credentials
└─ Output: Student can log in with new credentialsVIII. Session Coordination with FEAT-IDEN-002
After this FEAT succeeds, session state is:
onboarding_seat_ref= cleared (recovery has no Seat context)onboarding_user_ref = user_id(tells FEAT-IDEN-002 which user)recovery_student_ref= cleared (no longer needed)recovery_setup_authorization= nonce checked against the server record on every step
FEAT-IDEN-002 validates this state and activates new credentials.
IX. Difference from Initial Claim (FEAT-IDEN-001)
| Aspect | FEAT-IDEN-001 (Claim) | FEAT-IDEN-004 (Recovery) |
|---|---|---|
| Purpose | First claim of new seat | Regain access (lost creds) |
| User State | None yet; FEAT-IDEN-002 creates it | Already has old creds |
| Authorization | Name + dedupe code | Reset code |
| Seat | New, unclaimed | Not resolved |
| Output | seat_id and claim_generation for setup | user_id only for setup |
| Next Step | FEAT-IDEN-002 | FEAT-IDEN-002 |
Both workflows funnel to FEAT-IDEN-002: an initial claim to create and bind the User, and recovery to replace credentials.
X. Dependencies
docs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.mddocs/FEATURE-EXECUTION/FEAT-IDEN-003_TEACHER_RESET_CODE_GENERATION.mddocs/FEATURE-EXECUTION/FEAT-IDEN-002_STUDENT_CREDENTIAL_SETUP.mddocs/DOMAIN/DOM-IDEN-002_STUDENT_IDENTITY_ARCHITECTURE.mddocs/DOMAIN/DOM-IDEN-005_IDENTITY_BINDING_AND_LIFECYCLE.md
XI. Implementation Checklist
Before code review, verify:
- Generic error message used for all failures (no user existence leaks)
- Reset code is validated (not expired, exists, not already used)
- All existing credentials remain unchanged at acceptance
- Reset code is consumed atomically with server-side nonce creation
- No Seat, class, or profile is read or mutated during redemption/setup
- Audit event is emitted with outcome
- Reset code is NOT logged
- Rate limiting is enforced (max 5 per 15 min per IP)
- Lockout occurs after repeated failures (3 attempts + 5 min wait)
- Concurrent acceptance has one winner; consumed code never authorizes a new session
- Session state is prepared for FEAT-IDEN-002
- Tests cover success, expired code, invalid code, and replay scenarios
- Tests verify generic error messages (no user existence leaks)
XII. Amendments
Revisions to this document SHALL:
- Increment the version.
- Update the effective date.
- Maintain consistency with DOM-IDEN-002 §IX.
- Maintain consistency with FEAT-CORE-000.
- Maintain consistency with FEAT-IDEN-003 and FEAT-IDEN-002.
Version 1.4 (2026-10-01), operator ruling 2026-10-01: §II.0 makes the absence of an authenticated
principal a precondition of code acceptance (DOM-IDEN-005 v2.3 §VII): a signed-in browser is refused
without consuming the code. §V adds SIGNED_IN.
Version 1.3 (2026-09-28): §IX follows FEAT-IDEN-001 v3.0 and FEAT-IDEN-002 v2.0. An initial claim has no User until FEAT-IDEN-002 creates one.
Version 1.2 (2026-09-15): consume the code at acceptance and bind reset authority to a server-validated session nonce.
Last updated: 2026-09-30