FEAT-IDEN-001: Unauthenticated Student Seat Claim
[v2 - Compliant with INV-ARC-019 and DOM-IDEN Authority]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-001 | 3.1 | 2026-10-01 | 3.0 | Normative | ACTIVE |
I. Purpose
This FEAT is the verification phase of the unauthenticated student claim. It resolves a teacher-provided join code and the student’s roster name to exactly one unclaimed Seat, and hands that Seat to FEAT-IDEN-002 (Student Credential Setup). FEAT-IDEN-002 then creates the student’s User, activates its credentials and binds the Seat, all in one transaction.
This FEAT writes nothing. It provisions no User, binds no Seat and clears no claim artifact. 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”. A User is therefore never created before it has credentials.
Important
Scope Clarification: This FEAT handles unauthenticated claim only. It is for students claiming a classroom seat without prior authentication. For authenticated users joining a new classroom (reusing existing credentials), see FEAT-IDEN-005 (Authenticated Class Binding).
Governing Authority:
- INV-ARC-019 §VII, §X and §XII (Operational Actor, claim artifacts, Roster Provisioning and Seat Claim)
- DOM-IDEN-005 §V, §VII and §VIII (Membership by Existence, Student Identity Lifecycle, Identity Binding)
- DOM-IDEN-002 §VIII (Roster Provisioning and Claim)
- SPEC-SEC-001 §V.2 (lookup normalization and HMAC encoding)
- 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), the unauthenticated claim 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 seat resolution, 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
join_code: Valid join code for the target class.credentials:first_name: Student’s first name (full or partial, as provided by teacher roster)last_name: Student’s last name (full or partial, as provided by teacher roster)dedupe_code: (Optional) Short code provided by teacher to disambiguate duplicate names
idempotency_key: Accepted or generated per FEAT-CORE-000 §III.3.
Prohibited Inputs: None of the following may be accepted:
- ❌ DOB or DOB derivatives (per DOM-IDEN-002 §XI)
- ❌
existing_user_id(unauthenticated claim cannot reference existing users) - ❌
identity_hash(no cross-user identity inference)
2. Resolved Context (MANDATORY)
The FEAT MUST resolve the following before mutation:
class_id: Resolved viajoin_codeat theClassEconomyboundary (the only use ofjoin_code; every later step usesclass_id)roster_seat_id: The unclaimed seat in that class matching the claim hashesclaim_generation: The resolved Seat’s current server-stored generation (DOM-IDEN-005 §VIII; DOM-IDEN-007)
III. Orchestration Logic
A. Verification (Read-Only)
This FEAT has only a read-only phase. It SHALL perform no database write.
Step 1: Resolve Class
- Look up the
ClassEconomywhosejoin_codematches the provided code. - Take
class_idfrom it. - Failure Behavior: If no class is found, abort with
INVALID_JOIN_CODE.
Step 2: Resolve Roster Seat
- Select the unclaimed seats in the class:
class_idmatches, anduser_id IS NULL(DOM-IDEN-002 §VIII, Student Seat State).- Failure Behavior: If there are none, abort with
NO_UNCLAIMED_SEATS.
- Failure Behavior: If there are none, abort with
- Compute the claim hashes:
claim_first_name_hash = hash_claim_name(first_name, class_id=class_id, field="first")claim_last_name_hash = hash_claim_name(last_name, class_id=class_id, field="last")- Both use the normalization and purpose-separated HMAC encoding in SPEC-SEC-001 §V.2.
- Select the seats whose two claim hashes both match.
- Deduplication:
- If exactly one seat matches, use it.
- If several match and no
dedupe_codewas given, abort withAMBIGUOUS_IDENTITY. - If several match, keep those whose
dedupe_codeequals the given code. Exactly one must remain; otherwise abort withINVALID_DEDUPE_CODE. - If none match, abort with
INVALID_CREDENTIALS.
- Take
roster_seat_idand itsclaim_generationfrom the resolved seat. - Failure Behavior: Abort at the first failing step. A failure reveals no
User, no seat identifier and no other student’s data (see §VII on what a claimant can learn).
Step 3: Identity Inference Prohibition
Per DOM-IDEN-005 §VII:
“unauthenticated claim SHALL NOT search for or infer existing User identities outside the current claim transaction.”
This FEAT SHALL NOT:
- ❌ Search for, read or compare any
Userrecord - ❌ Compute or use an identity hash (DOB-based or otherwise)
- ❌ Reuse or link to any pre-existing
User
Rationale: An unauthenticated context cannot prove that a returning student is the same person. Identity merging is therefore prohibited, and FEAT-IDEN-002 always creates a new User for an unauthenticated claim.
Step 4: Hand-off to FEAT-IDEN-002
- Record the resolved Seat reference and its
claim_generationin the signed onboarding session (see Claim verification lifetime below). - Record no
Userreference. None exists yet for this claim.
B. What this FEAT does not do
The following belong to FEAT-IDEN-002, in one atomic transaction (DOM-IDEN-005 §VIII; INV-ARC-019 §X and §XII):
- creating the student
Usertogether with its credentials; - binding the Seat (
user_id,claimed_at) under a row lock, after rechecking that it is still unclaimed and that itsclaim_generationis the one recorded here; - clearing
claim_first_name_hash,claim_last_name_hash,roster_fingerprintanddedupe_code; - initializing
last_active_class_idandlast_active_seat_id(DOM-IDEN-002 §VIII, Claim Flow step 9).
Membership is not a record. Per DOM-IDEN-005 §V and INV-ARC-013, participation in a class is the existence of the bound Seat. Neither this FEAT nor FEAT-IDEN-002 creates a separate membership row. The v1 ClassMembership table does not exist.
IV. Invariants & Constraints
1. Read-Only (MANDATORY)
This FEAT SHALL NOT write to the database. A verified claim that is never completed leaves no User, no binding and no changed claim artifact.
2. No User Before Credentials (MANDATORY)
No User is provisioned by this FEAT. Per DOM-IDEN-005 §V and §VIII, a User exists only as an authenticated participant bound to a Seat.
3. Class Scope (MANDATORY)
Every seat query is scoped by the class_id resolved in Step 1. The claim hashes include class_id, so the same name matches no seat in any other class (SPEC-SEC-001 §V.2).
4. One Seat per User per Class
Per DOM-IDEN-001 §VII (“One User may own at most one Seat within a Class”) and DOM-IDEN-005 §VIII (“One User SHALL own at most one Seat within the same Class”). This is enforced at binding by FEAT-IDEN-002 and by UNIQUE(user_id, class_id) on seats.
5. No Identity Inference (MANDATORY)
Per DOM-IDEN-005 §VII, as in Step 3. No DOB or DOB-derived data is accepted (DOM-IDEN-002 §XI).
V. Idempotency
Per FEAT-CORE-000 §III.3, an idempotency_key is accepted or generated. Because this FEAT writes nothing, a replay has no effect to duplicate: the same inputs resolve to the same Seat, or to the same failure.
Replay safety for the claim itself belongs to FEAT-IDEN-002. The binding is conditional on Seat.user_id IS NULL and on the recorded claim_generation, so a replayed or stale verification cannot bind twice. An Unclaim invalidates every earlier verification by incrementing the generation.
VI. Audit Requirements
Per FEAT-CORE-000 §III.4, the verification attempt MUST emit an audit record via DOM-OPS (ACT-IDEN-001):
| Field | Type | Required | Rationale |
|---|---|---|---|
feat_id |
String | ✓ | Always “FEAT-IDEN-001” |
class_id |
UUID | When resolved | The classroom context |
seat_id |
Integer | On success | The verified, still unclaimed seat |
correlation_id |
String | ✓ | Links this verification to the FEAT-IDEN-002 completion |
idempotency_key |
String | ✓ | Replay detection |
outcome |
Enum | ✓ | SEAT_VERIFIED, or the failure code from §VII |
timestamp |
ISO 8601 | ✓ | UTC time of the attempt |
user_id |
UUID | ✗ | None exists; the actor is unauthenticated |
join_code |
String | ✗ | Not needed once class_id is resolved; keep it out of the record |
first_name, last_name, dedupe_code |
String | ✗ | DO NOT log (PII and claim material) |
Explicitly prohibited outcomes:
- ❌
EXISTING_USER_LINKED,USER_REUSED: identity inference is prohibited (DOM-IDEN-005 §VII). - ❌
NEW_USER_CLAIMED: this FEAT provisions no User. The claim completes in FEAT-IDEN-002.
VII. Failure Scenarios
A failure performs no write. It emits the audit record in §VI and returns an error. Messages SHALL NOT
reveal any User, seat identifier, class membership beyond the submitted join code, or another student’s data.
What a claimant can learn is bounded by the flow itself. Anyone holding the join code learns that a submitted
roster name matches, because a match proceeds to setup. AMBIGUOUS_IDENTITY adds only that the name matches
more than one seat, and it is required: DOM-IDEN-002 §VIII (Claim Flow step 6 and DOPO Handling) makes the
teacher-issued dedupe code mandatory for duplicate names, so the claimant must be asked for it. The join code is
the first gate on this surface. Every submission must also pass Cloudflare Turnstile verification
(student.claim_account), and the application-wide default rate limit applies.
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| Invalid join code | INVALID_JOIN_CODE |
400 | “Invalid join code or all seats already claimed. Check with your teacher.” |
| No unclaimed seats in class | NO_UNCLAIMED_SEATS |
400 | “Invalid join code or all seats already claimed. Check with your teacher.” |
| Names match no unclaimed seat | INVALID_CREDENTIALS |
400 | “No matching account found. Please check your join code and credentials.” |
| Several matches, no dedupe code given | AMBIGUOUS_IDENTITY |
400 | “Multiple students in this class share that name. Enter your deduplication code from your teacher.” |
| Dedupe code matches no single seat | INVALID_DEDUPE_CODE |
400 | “Invalid deduplication code. Check with your teacher.” |
| Database failure | INTERNAL_ERROR |
500 | “An error occurred during account claim. Please try again or contact support.” |
| 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. |
A seat claimed concurrently between verification and setup is refused by FEAT-IDEN-002 (INVALID_SEAT_STATE), not here.
VIII. 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-002_STUDENT_CREDENTIAL_SETUP.mddocs/DOMAIN/DOM-IDEN-001_CANONICAL_IDENTITY_MODEL.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/SPEC/SPEC-SEC-001_CREDENTIALS_AND_IDENTITY_LOOKUP_CODE_CONTRACT.md
IX. Implementation Checklist
Before code review, verify:
- The verification performs no database write
- No
Useris read, searched for, created or reused - No DOB or DOB-derived field is accepted or used
- Every seat query is scoped by the resolved
class_id - Claim hashes use
hash_claim_namewithclass_id(SPEC-SEC-001 §V.2) - The Seat reference and
claim_generationare stored in the signed onboarding session - Failure messages do not reveal whether a name or seat exists
- The
ACT-IDEN-001audit record is emitted without PII - No membership record is created (membership is the bound Seat)
X. Amendments
Revisions to this document SHALL:
- Increment the version.
- Update the effective date.
- Stay consistent with INV-ARC-019, DOM-IDEN-005 §V, §VII and §VIII, and DOM-IDEN-002 §VIII.
- Stay consistent with FEAT-CORE-000 and FEAT-IDEN-002.
Version 3.1 (2026-10-01), operator ruling 2026-10-01: §II.0 makes the absence of an authenticated
principal a precondition (DOM-IDEN-005 v2.3 §VII), checked before verification; §VII adds SIGNED_IN.
Incident OPS-DB-001: a signed-in browser ran this verification and FEAT-IDEN-002 created a second User.
Version 3.0 (2026-09-28):
- FEAT-IDEN-001 is the read-only verification phase of the claim. User provisioning, seat binding,
claim-artifact clearing and context initialization moved to FEAT-IDEN-002, which already
performed them. Version 2.x put an uncredentialed
Userand the binding here. That contradicted DOM-IDEN-005 §VIII (binding associates an authenticated User) and INV-ARC-019 §XII. - §VII states what a claimant can learn, and why
AMBIGUOUS_IDENTITYis required (DOM-IDEN-002 §VIII). - Removed the step that created a
ClassMembership“or equivalent” record. Membership is the bound Seat (DOM-IDEN-005 §V, INV-ARC-013), andClassMembershipdoes not exist in v2. - Removed the
IdempotencyRecordentity, because a read-only phase has nothing to replay. Idempotency is stated per FEAT-CORE-000 §III.3, and the binding’s replay guard (claim_generation) is in FEAT-IDEN-002. - The audit record now describes a verification attempt: it records no
user_idand nojoin_code. Types now follow the schema (users.idhas been a UUID since v2.0.1). - Corrected citations: DOM-IDEN-001 §VII says “may”, not “SHALL”. Context initialization is DOM-IDEN-002 §VIII, Claim Flow step 9. Added INV-ARC-013, INV-ARC-019, SPEC-SEC-001 and FEAT-IDEN-002 as dependencies.
Version 1.0 (2026-04-23) was withdrawn for constitutional non-compliance.
Claim verification lifetime
Successful verification captures the Seat’s server-stored claim_generation in the signed onboarding session alongside its Seat reference. FEAT-IDEN-002 must recheck this generation before binding; Unclaim invalidates all earlier verification sessions by incrementing it.
Last updated: 2026-09-30