FEAT-IDEN-002: Student Credential Setup
[v2 - Compliant with INV-ARC-019 and DOM-IDEN Authority]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-002 | 2.3 | 2026-10-01 | 2.2 | Normative | ACTIVE |
I. Purpose
This FEAT completes the unauthenticated student claim. FEAT-IDEN-001 (Unauthenticated Student Seat Claim) verified the claim and wrote nothing. In one transaction, this FEAT then creates the student User with its username, PIN and passphrase, and binds it to the Seat that FEAT-IDEN-001 verified.
Per DOM-IDEN-005 §VIII, binding associates an authenticated User with an unclaimed Seat. Per INV-ARC-019 §XII, “claim/setup later proves entitlement to the seat and creates/binds the authentication principal.” No User exists for an initial claim before this FEAT runs. DOM-IDEN-002 §VI permits provisioning a users row before claim, and v2 does not use that permission.
It also replaces credentials on an existing User after FEAT-IDEN-004 validates a recovery code. Recovery has a User reference and server-validated recovery nonce: it MUST NOT resolve, require, verify, or mutate a Seat, class, or IdentityProfile. All seat/class requirements below apply exclusively to initial claim, not recovery (DOM-IDEN-002 §IX; INV-ARC-019 §VI).
Governing Authority:
- DOM-IDEN-002 §VI, §VIII.IV & §IX (Student Identity Layers, Claim Flow, and Recovery)
- DOM-IDEN-005 §V, §VII and §VIII (Membership by Existence, Student Identity Lifecycle, Identity Binding)
- INV-ARC-019 §X and §XII (claim artifacts; Roster Provisioning and Seat Claim)
- SPEC-SEC-001 §V (credential hashing and lookup digests)
- SPEC-IDEN-001 (username retention verification; incorporated) and INV-ARC-018 §IX (memory-only setup staging)
- 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), credential setup, on both the initial-claim and the recovery path, 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 username generation or retention check, 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
setup_tokenandsetup_generation: opaque capability and generation of the consumed server-side retention proof (SPEC-IDEN-001). The FEAT MUST recheck its live completion record, scope, generation, and canonical username under the identity locks before mutation.user_id: Recovery only: the existing student User authorized by FEAT-IDEN-004. MUST be absent for initial claim, because no User exists yet.seat_idandclaim_generation: Initial claim only, taken from the signed onboarding session that FEAT-IDEN-001 wrote. MUST be absent in recovery.class_id: Initial claim only. MUST NOT be resolved for recovery.credentials:username: Chosen username (generated by UI or entered by student).pin: 4-6 digit PIN for non-monetary and reversible actions (see Credential boundary).passphrase: Passphrase for account authentication and irreversible expense actions (separate from PIN).
Credential boundary
The credential assignment is fixed and normative:
| Credential | Required actions |
|---|---|
| PIN | Clock in, take a break, mark done for the day, transfer between checking and savings, and other non-monetary or reversible actions. |
| Passphrase | Log in; store purchases; insurance purchases and payments; rent payments; using an entitlement except hall passes; and cancelling insurance. |
Hall-pass use is the explicit exception to the entitlement rule and uses the PIN. The application and user guides MUST use this matrix rather than treating all account actions or all transfers as one credential class.
idempotency_key: Client-provided unique request ID for retry safety.
2. Resolved Context (MANDATORY)
For recovery, resolve only the User referenced by the recovery session and verify
its nonce against recovery_setup_nonce_hash and recovery_setup_expires_at on
every setup request. A User reference or valid session cookie alone is insufficient.
Reject mixed initial-claim and recovery references; do not infer a User from a
Seat or fall back to a Seat bound to the User.
For initial claim, the FEAT MUST:
- Read the
Seatmatching the session’sseat_idwithout a lock, only to learn itsclass_id. Thatclass_idcomes from the database, never from the request. A Seat’sclass_idis fixed at provisioning: bindings do not migrate between classes (DOM-IDEN-005 §VIII), and no command reassigns it. - Lock that
ClassEconomyrow, then lock and reload theSeat, and useclass_idfrom the locked Seat. The class lock is always taken before the Seat lock, the order every class-scoped command uses. A Seat deleted between the read and the lock fails withINVALID_SEAT_STATE. - Resolve no
User. None exists for an initial claim, and none may be inferred (DOM-IDEN-005 §VII).
III. Orchestration Logic
A. Verification Phase (Read-Only)
Username retention prerequisite
Before credential activation on either initial claim or recovery, the server MUST
verify that the student reproduced the generated username after it was removed
from the verification surface. An acknowledgment cannot satisfy this gate.
SPEC-IDEN-001_USERNAME_RETENTION_VERIFICATION is incorporated for the interaction,
canonical matching, session and page binding, privacy, accessibility accommodation,
and required verification. A proof from another setup generation or page MUST NOT
activate credentials. Normal authentication fields continue to permit paste.
The server compares canonical username lookup digests using SPEC-SEC-001 §V.2;
case is significant. Security events and idempotency keys MUST contain neither
raw usernames nor lookup digests (INV-ARC-018 §V / SPEC-SEC-001 §V.5).
The temporary attempt is owned by Identity and memory-only under INV-ARC-018 §IX. A compare-and-set consumes the page proof and erases its username before activation. The volatile completion record retains only a lookup digest to bind the in-flight value; the FEAT rechecks it after acquiring the identity locks. Expired or replaced attempts cannot activate credentials, including after a lock wait; refused mutations may restore only the same, still-live attempt without extending its deadline. Successful activation erases all attempts for its Seat (claim) or User (recovery). Storage loss, expiry, unsafe configuration or scope mismatch fails closed. No migration, recoverable database field or cookie fallback is allowed.
Step 1: Validate User State
For recovery, existing credentials may still be present. Require the current
server-held recovery nonce verifier and unexpired deadline instead of NULL
credentials. At completion lock/reload the User and recheck authorization and
expiry, then replace all four credential fields, rotate current_session_nonce,
and clear recovery_setup_nonce_hash / recovery_setup_expires_at atomically.
Concurrent completions have one winner. A failed transaction or username collision
leaves old credentials and nonce authority intact for retry within the deadline.
For recovery, the User must have user_role = 'student'. Abort with INVALID_RECOVERY_STATE if the
authorization is missing, wrong or expired, or if a Seat reference is also present.
For initial claim there is no User to validate. A User reference present in an initial claim is rejected.
Step 2: Validate Seat State (Initial Claim Only)
On the locked Seat:
- Verify
user_id IS NULLandclaimed_at IS NULL(still unclaimed; DOM-IDEN-002 §VIII, Student Seat State). - Verify
role = 'student'. - Verify the session’s
claim_generationis an integer equal to the Seat’s currentclaim_generation(see Claim verification lifetime). - Failure Behavior: Abort with
INVALID_SEAT_STATE, without provisioning a User, if any check fails. This includes a seat claimed concurrently since verification.
Step 3: Validate Username Uniqueness
users.username_lookup_hashandusers.username_hashareUNIQUE. A collision on write is the uniqueness check.- Failure Behavior: On a collision, roll back to the savepoint and return
USERNAME_TAKEN. Recovery keeps its authorization for a retry within the deadline.
Step 4: Validate Credentials
- Username:
- Must be 3-32 characters.
- Must contain only alphanumeric characters, dashes, underscores.
- Must not be a reserved system word.
- PIN:
- Must be 4-6 digits.
- Must not be all the same digit (e.g., “1111”).
- Must not be sequential (e.g., “1234”).
- Passphrase:
- Must be 8+ characters.
- Must contain at least one letter and one number.
- Must not contain the username.
- Failure Behavior: Return validation errors with specific failure reason.
B. Mutation Phase (Atomic Transaction)
All mutations in this section MUST occur within a single database transaction.
Step 1: Hash Credentials
Per SPEC-SEC-001 §V:
username_hashandusername_lookup_hashboth come frombuild_hashed_username_fields(username). The first is a salted HMAC digest; the second is the unsalted, case-sensitive HMAC-SHA-256 lookup digest underPEPPER_KEY, after the canonical normalization in §V.2 (NFKC and trim; usernames are not lowercased).pin_hash = hash_password(pin)andpassphrase_hash = hash_password(passphrase)use scrypt (scrypt:32768:8:1) with a per-hash random salt. Password hashing MUST NOT usePEPPER_KEYor any other application secret (SPEC-SEC-001 §V, item 3).
Recovery may hash before taking the User row lock, to keep the lock short.
Step 2: Activate User Credentials
- Initial claim: create the
Userwithuser_role = 'student'and all four credential hashes. - Recovery: on the locked, reauthorized User, replace all four hashes. Clear the reset code and the
recovery nonce verifier and deadline, and rotate
current_session_nonce.
Per DOM-IDEN-002 §VI:
“Student-specific fields:
pin_hash,passphrase_hash”
Step 3: Bind the Seat (Initial Claim Only)
On the locked Seat, in the same transaction as Step 2 (DOM-IDEN-005 §VIII):
- Set
user_idto the new User andclaimed_at = NOW().UNIQUE(user_id, class_id)holds one seat per user per class. - Clear
claim_first_name_hash,claim_last_name_hash,roster_fingerprintanddedupe_code(INV-ARC-019 §X). - Set
User.last_active_class_idandUser.last_active_seat_idto this class and seat (DOM-IDEN-002 §VIII, Claim Flow step 9; DOM-IDEN-006 §XIII). - Create no membership record. Participation is the bound Seat (DOM-IDEN-005 §V; INV-ARC-013).
Recovery skips this step entirely. It must not read, clear or regenerate claim material.
Step 4: Clear Onboarding State (Session)
The route handler SHALL clear session state used during onboarding:
- Clear
onboarding_seat_ref - Clear
onboarding_user_ref - Clear
recovery_setup_authorization - Erase the volatile setup attempt and clear
student_setup_token; no readable username or proof is stored in the cookie - Clear
theme_promptandtheme_slug(if used)
Note: This is application-level housekeeping, not a database mutation.
Step 5: Context Establishment
Establish the authenticated session context:
- Set
current_session_started_at = NOW()on the User (per DOM-IDEN-002 §VII). - Set
current_session_expires_at = NOW() + SESSION_DURATION(fixed window, does not slide). - Generate
current_session_nonce(random token binding this session).
Per DOM-IDEN-002 §VII (Session Establishment):
“
current_session_started_atis written at sign-in.” “current_session_expires_atis written once at sign-in (fixed window, does not slide forward on activity).” “current_session_nonceis regenerated at sign-in…”
Step 6: Audit Trace
Per FEAT-CORE-000 §III.4:
- Call
DOM-OPSto emit anACT-IDEN-002audit event. - Required fields:
feat_id: “FEAT-IDEN-002”user_id: The activateduser_idseat_id: The claimedseat_id(initial claim only)class_id: The classroom context (initial claim only)idempotency_key: The provided keyoutcome:"CREDENTIAL_ACTIVATED"(only possible outcome)timestamp: ISO 8601 UTC timestamp
Note: DO NOT log the actual username, PIN, or passphrase.
IV. Invariants & Constraints
1. Credential Activation (MANDATORY)
Once this FEAT completes, the student can sign in with their username and passphrase (per DOM-IDEN-002 §VII).
2. Initial-Claim Requirement (MANDATORY)
For initial claim, this FEAT SHALL ONLY bind a Seat that is:
- still unclaimed (
user_id IS NULL,claimed_at IS NULL), rechecked under lock; - at the
claim_generationthat FEAT-IDEN-001 recorded.
It always creates a new User (DOM-IDEN-005 §VII).
3. Username Uniqueness (MANDATORY)
users.username_lookup_hash is UNIQUE, so only one account can hold a given username.
4. Single Activation (MANDATORY)
Once activated, credentials SHALL NOT be re-activated by this FEAT. Re-setting credentials requires prior authorization through FEAT-IDEN-004; recovery does not require a Seat.
5. Atomic Transaction (MANDATORY)
All credential mutations SHALL occur in a single transaction. If any step fails, complete rollback occurs.
V. Idempotency
Per FEAT-CORE-000 §III.3, an idempotency_key is accepted or generated, and a duplicate execution produces no duplicate effect. The mechanism is the state each path consumes, not a separate record store:
- Initial claim. Binding is conditional on a locked, still-unclaimed Seat at the recorded
claim_generation. A replay after success finds the Seat bound and fails withINVALID_SEAT_STATEwithout creating a second User. A failed attempt rolls back entirely, and the student retries. - Recovery. Completion consumes the recovery nonce verifier in the credential transaction. A replay finds no authorization and fails with
INVALID_RECOVERY_STATE. A failed attempt leaves the authorization intact for a retry within the deadline.
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 student past the credential setup step.
Failure Cases:
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| Recovery authorization missing, wrong, expired or mixed with a Seat reference | INVALID_RECOVERY_STATE |
400 | “Invalid or expired recovery code.” |
Seat no longer unclaimed, or stale/missing claim_generation |
INVALID_SEAT_STATE |
409 | “Invalid setup state. Please start over.” |
| Username taken | USERNAME_TAKEN |
400 | “That username is already taken. Please choose another.” |
| Username invalid | INVALID_USERNAME |
400 | “Username must be 3-32 characters, alphanumeric only.” |
| PIN invalid | INVALID_PIN |
400 | “PIN must be 4-6 digits, not all the same or sequential.” |
| Passphrase invalid | INVALID_PASSPHRASE |
400 | “Passphrase must be 8+ characters with letters and numbers.” |
| User/seat mismatch | CONTEXT_MISMATCH |
400 | “Setup state mismatch. Please start over.” |
| Database error | INTERNAL_ERROR |
500 | “An error occurred during setup. 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. |
VII. Audit Requirements
The DOM-OPS audit log MUST contain:
| Field | Type | Required | Rationale |
|---|---|---|---|
feat_id |
String | ✓ | Identifies the FEAT (always “FEAT-IDEN-002”) |
user_id |
UUID | ✓ | The created (initial claim) or recovered user |
seat_id |
Integer | Initial claim only | The claimed seat |
class_id |
UUID | Initial claim only | The classroom context |
idempotency_key |
String | ✓ | Replay detection |
outcome |
Enum | ✓ | Must be: CREDENTIAL_ACTIVATED (only valid outcome) |
timestamp |
ISO 8601 | ✓ | UTC timestamp of activation |
username |
String | ✗ | DO NOT log (PII) |
pin |
String | ✗ | DO NOT log (security) |
passphrase |
String | ✗ | DO NOT log (security) |
error_code |
String | ⚠️ | Only if outcome is FAILED |
Outcomes:
CREDENTIAL_ACTIVATED: the only successful outcome. For initial claim, the User was created with credentials and bound. For recovery, the credentials were replaced.FAILED: any failure, witherror_codeset to a code from §VI.
VIII. Integration with FEAT-IDEN-001
FEAT-IDEN-002 is the second step of the student claim flow:
Step 1: FEAT-IDEN-001 (Unauthenticated Seat Claim, read-only verification)
├─ Input: join_code, first_name, last_name, optional dedupe_code
├─ Output: seat_id + claim_generation, in the signed onboarding session
└─ User state: none exists; nothing is written
Step 2: FEAT-IDEN-002 (Credential Setup, one transaction)
├─ Input: seat_id + claim_generation (from session), consumed retention proof
│ (setup_token, SPEC-IDEN-001), username, pin, passphrase
├─ Effects: User created with credentials, Seat bound, claim artifacts cleared,
│ last_active pointers set
└─ User state: authenticated participant, ready to log inThe claim completes only when FEAT-IDEN-002 commits. A verification that is never completed leaves no trace in the database.
IX. Relationship to Account Recovery
FEAT-IDEN-003 issues a reset code: the teacher selects a student Seat,
which resolves to its bound User. The code is stored on that User.
FEAT-IDEN-004 consumes the student’s code and supplies
onboarding_user_ref plus a unique session nonce to this FEAT. Credential replacement updates that same
User without resolving or changing participation. A stale onboarding_seat_ref
must be cleared at redemption; mixed references must be rejected at setup.
The nonce verifier and expiry on User are the source of truth throughout setup. Successful replacement clears that server record in the credential transaction. Teacher reissuance invalidates outstanding nonce authority. A new session cannot reuse the consumed code; it needs a new teacher-issued code.
X. Dependencies
docs/INVARIANT/ARCHITECTURE/INV-ARC-013_MEMBERSHIP_BY_EXISTENCE.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-019_IDENTITY_AND_OWNERSHIP_MODEL.mddocs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.mddocs/FEATURE-EXECUTION/FEAT-IDEN-001_UNAUTHENTICATED_STUDENT_SEAT_CLAIM_REMEDIATED.mddocs/DOMAIN/DOM-IDEN-002_STUDENT_IDENTITY_ARCHITECTURE.mddocs/DOMAIN/DOM-IDEN-005_IDENTITY_BINDING_AND_LIFECYCLE.mddocs/DOMAIN/DOM-IDEN-006_CANONICAL_CONTEXT_RESOLUTION.mddocs/INVARIANT/ARCHITECTURE/INV-ARC-018_PII_STORAGE_AND_RETENTION_ENFORCEMENT.mddocs/SPEC/SPEC-SEC-001_CREDENTIALS_AND_IDENTITY_LOOKUP_CODE_CONTRACT.mddocs/SPEC/SPEC-IDEN-001_USERNAME_RETENTION_VERIFICATION.md
XI. Implementation Checklist
Before code review, verify:
- Initial claim creates the User only after the locked Seat is confirmed unclaimed at the recorded
claim_generation - Seat validation applies only to initial claim; recovery performs no participation lookup
- Credentials activate only after a consumed, live SPEC-IDEN-001 retention proof, rechecked under the identity locks
- Username uniqueness is enforced
- Credential validation rules are applied
- PIN and passphrase are hashed with
hash_password()(scrypt, salted, never peppered; SPEC-SEC-001 §V) - Session state (started_at, expires_at, nonce) is initialized
- All mutations occur in a single transaction
- Audit event is emitted with all required fields
- Rollback occurs on any failure
- Credentials are NOT logged in audit
- A replayed initial claim or recovery completion creates no second User and changes nothing
- Tests cover successful and failed scenarios
- Tests cover replay after success and a stale
claim_generation - No membership record is created
XII. Amendments
Revisions to this document SHALL:
- Increment the version.
- Update the effective date.
- Maintain consistency with INV-ARC-019, DOM-IDEN-005 §V, §VII and §VIII, and DOM-IDEN-002 §VI, §VII and §VIII.
- Maintain consistency with FEAT-CORE-000.
- Maintain consistency with FEAT-IDEN-001.
Version 2.3 (2026-10-01), operator ruling 2026-10-01: §II.0 makes the absence of an authenticated
principal a precondition of every setup step (DOM-IDEN-005 v2.3 §VII); a setup begun signed out cannot
continue or complete after a sign-in in the same browser. §VI adds SIGNED_IN. Matching amendments:
FEAT-IDEN-001 v3.1, FEAT-IDEN-004 v1.4.
Version 2.2 (2026-09-29): §II.2 states the lock sequence: an unlocked read of the Seat’s database class_id, then the class lock, then the Seat lock. §VII lists FAILED beside the only successful outcome.
Version 2.1 (2026-09-29): §II and §IV.1: sign-in uses the username and passphrase, following DOM-IDEN-002 2.9 and the credential matrix. It had said username and PIN.
Version 2.0 (2026-09-29):
- Supersedes 1.5 and keeps its username retention gate,
setup_tokeninputs and session housekeeping unchanged. Lists SPEC-IDEN-001 and INV-ARC-018 as governing documents and dependencies. - For initial claim, this FEAT creates the
Userwith its credentials and binds the Seat in one transaction. Version 1.x (§I–§IV and §VIII) described activating credentials on a User that FEAT-IDEN-001 had already created and bound. That contradicted this document’s own v1.4 Claim verification lifetime section, DOM-IDEN-005 §VIII (binding associates an authenticated User) and INV-ARC-019 §XII. Matching amendment: FEAT-IDEN-001 v3.0. - Credential hashing now follows SPEC-SEC-001 §V: scrypt, salted, never peppered. It replaces “bcrypt with salt + pepper”, which that spec forbids.
- Removed the
IdempotencyRecordentity, which does not exist. Replay safety is stated through the consumed seat generation and recovery nonce. - Replaced the
INVALID_USER_STATEfailure withINVALID_RECOVERY_STATE. The audituser_idis now a UUID. Added INV-ARC-013, INV-ARC-019 and SPEC-SEC-001 as dependencies.
Version 1.5 (2026-09-29): added the Username retention prerequisite (SPEC-IDEN-001, incorporated): credentials
activate only after the student reproduces the generated username, with memory-only staging under INV-ARC-018 §IX.
Added the setup_token / setup_generation inputs; the username lookup digest is case-sensitive (SPEC-SEC-001 §V.2).
Version 1.3 (2026-09-15): server-validated recovery nonce consumed atomically with credential replacement.
Claim verification lifetime
For initial claim only, lock the resolved class and Seat and recheck that the Seat remains unclaimed and the signed onboarding session contains the current integer claim_generation. Missing or stale generations fail closed without provisioning a User or changing credentials. Recovery continues to use only its server-validated User authorization nonce.
Last updated: 2026-09-30