FEAT-IDEN-107: Teacher Revoke Passkey
[NEW - Compliant with DOM-IDEN Authority]
| Reference Number | Version | Effective Date | Supersedes | Authority Level | Status |
|---|---|---|---|---|---|
| FEAT-IDEN-107 | 1.2 | 2026-10-14 | 1.1 | Normative | NEW |
Important
v1.2 (2026-10-14), review correction. v1.1’s restated replay-safety mechanism was a read-then-act check, not a lock: two requests carrying the same idempotency_key can both pass Step 3’s existence lookup before either audit event is written, since audit_events carries no uniqueness on (feat_id, idempotency_key) (app/models.py AuditEvent.__table_args__). §V now requires a unique constraint on that pair as part of this FEAT’s schema contract, and Step 3’s failure behavior is restated to rely on that constraint rather than on the lookup alone.
v1.1 (2026-10-14), documentation modernization. Removed the former Mutation Phase Step 1, which inserted into an IdempotencyRecord entity that does not exist anywhere in the codebase (same removal already made in FEAT-IDEN-001/FEAT-IDEN-002). Replay safety is restated through the mechanism §V already described: a missing passkey_credentials row plus a matching idempotency_key in the ACT-IDEN-107 audit event (Step 2, Mutation Phase) resolves to ALREADY_REVOKED rather than CREDENTIAL_NOT_FOUND. Renumbered the remaining Mutation Phase steps; no behavior change.
I. Purpose
This FEAT allows a teacher to revoke (remove) an enrolled passkey credential from their account. Per DOM-IDEN-003 §III.B:
“Teachers can revoke passkey credentials at any time. Revoking a passkey does not affect TOTP (the mandatory factor).”
This FEAT deletes a single passkey_credentials record, disabling that authenticator for login. The teacher can still authenticate using TOTP alone.
Governing Authority:
- DOM-IDEN-003 §III.B (Teacher Authentication - Passkey Management)
- DOM-IDEN-005 §VIII (Identity Binding)
- FEAT-CORE-000 (Feature Execution Constitutional Directive)
II. Execution Context
1. Required Inputs
user_id: The teacherUserrevoking passkey (from session, authenticated).seat_id: The adminSeatin the target class (from context).class_id: The class context (from session or context).credential_id: The base64url-encoded credential ID of the passkey to revoke.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. - The
PasskeyCredentialrecord matchingcredential_idanduser_id. - 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: Lookup Passkey Credential
- Query
passkey_credentialswherecredential_id = credential_idanduser_id = user_id. - Verify the credential exists.
- Failure Behavior: Abort with
CREDENTIAL_NOT_FOUNDif the credential does not exist or belongs to a different user — unless anACT-IDEN-107audit event already exists for thisidempotency_keyper §V’s unique constraint, in which case return success with outcomeALREADY_REVOKEDinstead. A bare existence lookup with no uniqueness behind it is not sufficient — see §V.
Step 4: Verify Credential Ownership
- Ensure
passkey_credentials.user_id == user_id(credential belongs to this teacher). - Failure Behavior: Abort with
OWNERSHIP_MISMATCHif credential belongs to someone else (data integrity issue).
Step 5: Check Remaining Authentication Factors
- Query
User.totp_secret_encrypted. - Verify that TOTP is still enrolled (not NULL).
- Passkey is optional; revoking it should not leave the teacher unable to authenticate.
- If TOTP is NULL, this indicates a data integrity issue.
- Failure Behavior: Abort with
CANNOT_REVOKE_LAST_FACTORif revoking this passkey would leave no authentication methods.
Note: TOTP is mandatory, so it should always exist. This is a safety check.
B. Mutation Phase (Atomic Transaction)
All mutations in this section MUST occur within a single database transaction.
Step 1: Delete PasskeyCredential Record
Delete from passkey_credentials table:
- WHERE
credential_id = credential_idanduser_id = user_id. - Confirm zero or one row was deleted. Zero rows indicates the credential was already removed (acceptable for idempotent retries — see §V).
Per DOM-IDEN-003 §III.B:
“Passkey credentials can be revoked (deleted) at any time.”
Note: This is a hard delete. The credential is completely removed; there is no deleted_at soft-delete marker.
Step 2: Audit Trace
Per FEAT-CORE-000 §III.4:
- Call
DOM-OPSto emit anACT-IDEN-107audit event. - Required fields:
feat_id: “FEAT-IDEN-107”user_id: The teacheruser_idseat_id: The adminseat_idclass_id: The classroom contextauthenticator_name: The name of the revoked passkey (safe to log)credential_id_hash: A hash of the credential ID (optional, for identification without storing full ID)idempotency_key: The provided keyoutcome:"PASSKEY_REVOKED"(only possible outcome)timestamp: ISO 8601 UTC timestamp
Note: DO NOT log the full credential ID or public key.
IV. Invariants & Constraints
1. Optional Factor (MANDATORY)
Passkey is optional. Revoking all passkeys does not prevent authentication (TOTP is mandatory and sufficient).
2. Cannot Revoke Last Factor (MANDATORY)
While TOTP is mandatory, this check prevents data integrity issues where both TOTP and passkey are missing.
3. Ownership Verification (MANDATORY)
A teacher can ONLY revoke their own passkeys. Cross-user revocation is prevented.
4. Atomic Transaction (MANDATORY)
The deletion SHALL occur in a single transaction. If any step fails, complete rollback occurs.
5. Hard Delete (MANDATORY)
Revoked credentials are permanently deleted, not marked inactive. There is no recovery or restoration path.
6. Multiple Passkeys Allowed (MANDATORY)
A teacher can revoke individual passkeys without affecting others. Multiple passkeys can coexist.
V. Idempotency
Mechanism: A unique constraint on audit_events (feat_id, idempotency_key) is the idempotency lock, not the passkey_credentials existence check alone. A bare “does the row still exist” lookup is a read, not a lock: two concurrent requests carrying the same idempotency_key can both read “row gone” before either has written its ACT-IDEN-107 audit event, and both would then emit a duplicate event. Closing that race requires the database to reject the second event, which requires uniqueness on (feat_id, idempotency_key) — audit_events carries no such constraint today (app/models.py AuditEvent.__table_args__); adding one (partial, WHERE idempotency_key IS NOT NULL, since the column is nullable for non-idempotent FEATs) is part of this FEAT’s schema contract.
Behavior:
- If a retry occurs with the same
idempotency_key:- Look up an existing
ACT-IDEN-107audit event for thisidempotency_key(the authoritative replay signal, backed by the unique constraint above). - If found, return success with outcome
ALREADY_REVOKEDwithout touchingpasskey_credentialsagain. - If not found, proceed with the full deletion and audit-event insert inside the Mutation Phase’s single transaction; the unique constraint turns a lost race into a constraint violation the FEAT catches and resolves to
ALREADY_REVOKED, rather than a second audit event.
- Look up an existing
- Replayed requests with the same
idempotency_keySHALL NOT cause errors on retry, and SHALL NOT produce two audit events for one revocation, concurrent or not.
Client Responsibility:
- Generate a stable
idempotency_key(e.g., based on session and credential ID). - Retry on transient errors with the same key.
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 the passkey credential intact (no partial deletes).
Failure Cases:
| Scenario | Error Code | HTTP Status | Message |
|---|---|---|---|
| User not a teacher | INVALID_USER_ROLE |
403 | “Only teachers can manage passkeys.” |
| Invalid seat | INVALID_SEAT_STATE |
409 | “Invalid seat. Please start over.” |
| Credential not found | CREDENTIAL_NOT_FOUND |
404 | “Passkey not found. It may have already been removed.” |
| Ownership mismatch | OWNERSHIP_MISMATCH |
403 | “You don’t have permission to revoke this passkey.” |
| Cannot revoke last factor | CANNOT_REVOKE_LAST_FACTOR |
409 | “Cannot revoke the last authentication factor.” |
| Database error | INTERNAL_ERROR |
500 | “An error occurred. 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-107”) |
user_id |
Integer | ✓ | The teacher user |
seat_id |
Integer | ✓ | The admin seat |
class_id |
UUID | ✓ | The classroom context |
idempotency_key |
String | ✓ | Replay detection |
authenticator_name |
String | ✓ | User-provided passkey name (safe to log) |
credential_id_hash |
String | ⚠️ | Hash of credential ID for identification (optional) |
outcome |
Enum | ✓ | Must be: PASSKEY_REVOKED or ALREADY_REVOKED |
timestamp |
ISO 8601 | ✓ | UTC timestamp of revocation |
credential_id |
String | ✗ | DO NOT log (security) |
public_key |
String | ✗ | DO NOT log (security) |
error_code |
String | ⚠️ | Only if outcome is FAILED |
Outcomes:
PASSKEY_REVOKED: Successful passkey credential revocation.ALREADY_REVOKED: Idempotent return; credential already revoked.
VIII. Display Requirements
After successful passkey revocation, the route handler SHALL display to the teacher:
- Confirmation: “Passkey revoked successfully!”
- Authenticator Name: Show the name of the revoked passkey (e.g., “YubiKey 5”).
- Impact Message: “You can still authenticate using TOTP. This passkey will no longer work for login.”
- Option to Return: Link or button to return to passkey management view.
- Show list of remaining passkeys (if any).
- Option to enroll new passkeys.
IX. Integration with Passkey Management
Passkey revocation is part of credential management:
FEAT-IDEN-102: Enroll Passkey
└─ Teacher adds a new passkey credential
Teacher Passkey Management (EXISTING FEATS)
├─ View enrolled passkeys (read-only)
└─ Revoke individual passkeys (FEAT-IDEN-107)
FEAT-IDEN-107: Revoke Passkey (THIS FEAT)
└─ Remove a single passkey credentialMultiple passkeys can be managed independently. Revoking one does not affect others.
X. Security Considerations
Credential Deletion is Permanent
Once revoked, a credential cannot be recovered or restored. The teacher must re-enroll the same authenticator if they want to use it again.
Ownership Verification is Critical
The FEAT strictly verifies that the passkey belongs to the authenticated teacher. Cross-user revocation attempts are rejected.
Hard Delete vs. Soft Delete
This FEAT uses hard delete (permanent removal) rather than soft delete (marking inactive). This keeps the database clean and prevents accumulation of revoked credentials.
XI. Dependencies
docs/FEATURE-EXECUTION/FEAT-CORE-000_FEATURE_EXECUTION_CONSTITUTIONAL_DIRECTIVE.mddocs/FEATURE-EXECUTION/FEAT-IDEN-102_TEACHER_PASSKEY_ENROLLMENT.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 revoke
- Seat binding is verified before revocation
- Credential lookup by ID and user ownership
- Ownership verification prevents cross-user revocation
- TOTP existence check (prevent last-factor revocation)
- Hard delete from passkey_credentials table
- Exactly one row deleted (idempotency)
- All mutations occur in a single transaction
- Audit event emitted with all required fields
- Rollback occurs on any failure
- Credential ID is NOT logged (only hash if needed)
- Idempotency check handles already-revoked credentials
- Tests cover successful revocation
- Tests cover idempotent retry (already revoked)
- Tests cover ownership mismatch (cross-user attempt)
- Tests cover last-factor prevention
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-102 (Enroll Passkey).
This is version 1.0 of FEAT-IDEN-107 (new specification, 2026-08-09).
Last updated: 2026-10-14