Skip to content
FEAT-IDEN-002: Student Credential Setup

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_token and setup_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_id and claim_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 Seat matching the session’s seat_id without a lock, only to learn its class_id. That class_id comes from the database, never from the request. A Seat’s class_id is fixed at provisioning: bindings do not migrate between classes (DOM-IDEN-005 §VIII), and no command reassigns it.
  • Lock that ClassEconomy row, then lock and reload the Seat, and use class_id from 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 with INVALID_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:

  1. Verify user_id IS NULL and claimed_at IS NULL (still unclaimed; DOM-IDEN-002 §VIII, Student Seat State).
  2. Verify role = 'student'.
  3. Verify the session’s claim_generation is an integer equal to the Seat’s current claim_generation (see Claim verification lifetime).
  4. 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

  1. users.username_lookup_hash and users.username_hash are UNIQUE. A collision on write is the uniqueness check.
  2. 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

  1. Username:
    • Must be 3-32 characters.
    • Must contain only alphanumeric characters, dashes, underscores.
    • Must not be a reserved system word.
  2. PIN:
    • Must be 4-6 digits.
    • Must not be all the same digit (e.g., “1111”).
    • Must not be sequential (e.g., “1234”).
  3. Passphrase:
    • Must be 8+ characters.
    • Must contain at least one letter and one number.
    • Must not contain the username.
  4. 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_hash and username_lookup_hash both come from build_hashed_username_fields(username). The first is a salted HMAC digest; the second is the unsalted, case-sensitive HMAC-SHA-256 lookup digest under PEPPER_KEY, after the canonical normalization in §V.2 (NFKC and trim; usernames are not lowercased).
  • pin_hash = hash_password(pin) and passphrase_hash = hash_password(passphrase) use scrypt (scrypt:32768:8:1) with a per-hash random salt. Password hashing MUST NOT use PEPPER_KEY or 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 User with user_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):

  1. Set user_id to the new User and claimed_at = NOW(). UNIQUE(user_id, class_id) holds one seat per user per class.
  2. Clear claim_first_name_hash, claim_last_name_hash, roster_fingerprint and dedupe_code (INV-ARC-019 §X).
  3. Set User.last_active_class_id and User.last_active_seat_id to this class and seat (DOM-IDEN-002 §VIII, Claim Flow step 9; DOM-IDEN-006 §XIII).
  4. 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_prompt and theme_slug (if used)

Note: This is application-level housekeeping, not a database mutation.

Step 5: Context Establishment

Establish the authenticated session context:

  1. Set current_session_started_at = NOW() on the User (per DOM-IDEN-002 §VII).
  2. Set current_session_expires_at = NOW() + SESSION_DURATION (fixed window, does not slide).
  3. Generate current_session_nonce (random token binding this session).

Per DOM-IDEN-002 §VII (Session Establishment):

“current_session_started_at is written at sign-in.” “current_session_expires_at is written once at sign-in (fixed window, does not slide forward on activity).” “current_session_nonce is regenerated at sign-in…”

Step 6: Audit Trace

Per FEAT-CORE-000 §III.4:

  1. Call DOM-OPS to emit an ACT-IDEN-002 audit event.
  2. Required fields:
    • feat_id: “FEAT-IDEN-002”
    • user_id: The activated user_id
    • seat_id: The claimed seat_id (initial claim only)
    • class_id: The classroom context (initial claim only)
    • idempotency_key: The provided key
    • outcome: "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_generation that 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 with INVALID_SEAT_STATE without 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:

  1. Rollback all mutations atomically.
  2. Emit audit event with outcome FAILED and error code.
  3. Return error response to client.
  4. 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, with error_code set 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 in

The 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.md
  • docs/INVARIANT/ARCHITECTURE/INV-ARC-019_IDENTITY_AND_OWNERSHIP_MODEL.md
  • docs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.md
  • docs/FEATURE-EXECUTION/FEAT-IDEN-001_UNAUTHENTICATED_STUDENT_SEAT_CLAIM_REMEDIATED.md
  • docs/DOMAIN/DOM-IDEN-002_STUDENT_IDENTITY_ARCHITECTURE.md
  • docs/DOMAIN/DOM-IDEN-005_IDENTITY_BINDING_AND_LIFECYCLE.md
  • docs/DOMAIN/DOM-IDEN-006_CANONICAL_CONTEXT_RESOLUTION.md
  • docs/INVARIANT/ARCHITECTURE/INV-ARC-018_PII_STORAGE_AND_RETENTION_ENFORCEMENT.md
  • docs/SPEC/SPEC-SEC-001_CREDENTIALS_AND_IDENTITY_LOOKUP_CODE_CONTRACT.md
  • docs/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:

  1. Increment the version.
  2. Update the effective date.
  3. Maintain consistency with INV-ARC-019, DOM-IDEN-005 §V, §VII and §VIII, and DOM-IDEN-002 §VI, §VII and §VIII.
  4. Maintain consistency with FEAT-CORE-000.
  5. 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_token inputs and session housekeeping unchanged. Lists SPEC-IDEN-001 and INV-ARC-018 as governing documents and dependencies.
  • For initial claim, this FEAT creates the User with 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 IdempotencyRecord entity, which does not exist. Replay safety is stated through the consumed seat generation and recovery nonce.
  • Replaced the INVALID_USER_STATE failure with INVALID_RECOVERY_STATE. The audit user_id is 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.

Edit canonical source on GitHub

Last updated: 2026-09-30