BLOCKER: Re-enrolment must preserve the owner authority boundary #734

Open
opened 2026-10-02 13:06:53 +00:00 by kayg · 15 comments
Owner

Defensive sec-auth audit, base c4a61e8cf0, requested under #663. Source review only; no product edits.

A1 — BLOCKER: re-enrolment must preserve the owner boundary

Evidence at the base:

  • crates/calternal-auth/src/api.rs:1066 requires FreshAdmin to issue a
    re-enrolment link.
  • crates/calternal-auth/src/store.rs:2666 accepts any enabled admin as the
    actor. Its target check requires only an enabled User. It does not refuse
    the owner target.
  • crates/calternal-auth/src/store.rs:2701 consumes that grant and adds a
    credential to its target User.
  • In contrast, set_role at line 1400 requires the owner actor and excludes
    the owner target. set_disabled at line 1476 excludes the owner target.

Impact: the lower admin Role can grant a new credential for the owner
account. This defeats the owner-only authority boundary. A fresh admin
assertion authenticates the admin; it does not supply owner authority.
The same target check is present in round 7a (store.rs:3193).

Fix: refuse an admin actor for an owner target at issuance and consumption.
Bind the grant to the issuer and check the issuer's current authority when
it is consumed. Keep owner recovery under an explicit owner policy.

Required regression: a Role matrix for issuance and consumption, including
owner targets, issuer disable or demotion, and expired or consumed grants.
Retain the existing allowed member recovery test.

Duplicate check: searched all issue states for authentication, recovery, re-enrolment, auth_time and challenge findings; no matching corrective issue found. Related #3 and #195 are broad auth and authorization work.

Defensive sec-auth audit, base c4a61e8cf090170f35b1bed3350d9de20c83ecd5, requested under #663. Source review only; no product edits. ### A1 — BLOCKER: re-enrolment must preserve the owner boundary Evidence at the base: - `crates/calternal-auth/src/api.rs:1066` requires `FreshAdmin` to issue a re-enrolment link. - `crates/calternal-auth/src/store.rs:2666` accepts any enabled admin as the actor. Its target check requires only an enabled User. It does not refuse the owner target. - `crates/calternal-auth/src/store.rs:2701` consumes that grant and adds a credential to its target User. - In contrast, `set_role` at line 1400 requires the owner actor and excludes the owner target. `set_disabled` at line 1476 excludes the owner target. Impact: the lower admin Role can grant a new credential for the owner account. This defeats the owner-only authority boundary. A fresh admin assertion authenticates the admin; it does not supply owner authority. The same target check is present in round 7a (`store.rs:3193`). Fix: refuse an admin actor for an owner target at issuance and consumption. Bind the grant to the issuer and check the issuer's current authority when it is consumed. Keep owner recovery under an explicit owner policy. Required regression: a Role matrix for issuance and consumption, including owner targets, issuer disable or demotion, and expired or consumed grants. Retain the existing allowed member recovery test. Duplicate check: searched all issue states for authentication, recovery, re-enrolment, auth_time and challenge findings; no matching corrective issue found. Related #3 and #195 are broad auth and authorization work.
Author
Owner

Started authfix on job/authfix; base 2f4482ded0 (job/merge-round-7a). Scope: #734, #735, #737 and #738. Implement owner boundaries, signed OIDC authentication freshness, bounded ceremony retention, and admitted recovery verification with regression tests. No UI changes planned.

Started authfix on job/authfix; base 2f4482ded066d9c5d9c59130377907f7fd2916c9 (job/merge-round-7a). Scope: #734, #735, #737 and #738. Implement owner boundaries, signed OIDC authentication freshness, bounded ceremony retention, and admitted recovery verification with regression tests. No UI changes planned.
Author
Owner

Source findings: re-enrolment issuance and consumption had no owner-target restriction and consumption did not recheck created_by authority. Disable and deletion already exclude owner targets; set_role requires an owner actor and excludes owner targets; session revocation is self-scoped. The new regression covers those existing boundaries as well as the re-enrolment Role matrix and stale issuers.

OIDC ordinary session creation called mark_asserted without auth_time. The implementation removes that call, requests max_age=0 for explicit elevation, and checks signed auth_time with a 300-second freshness window and at most 30 seconds of clock lead. This follows DESIGN §21 and OpenID Connect Core §§2 and 3.1.2.1 (https://openid.net/specs/openid-connect-core-1_0.html).

Decisions for #737: share one Instance budget across passkeys and all OIDC providers: 512 flows, 16 flows per resolved client IP, 16 MiB conservative retained-state reservations, five-minute TTL. Reserve before construction. Evict expired entries in oldest-first order; return 429 at live capacity to preserve in-flight ceremonies. Bound credentials/options and input tokens. Recovery checks share the existing four-worker Argon2 memory bound and fail admission immediately at capacity; cancelled callers retain the permit in blocking work.

Cold calternal-auth build is running with four jobs and limited debug data. No gate pass is claimed yet.

Source findings: re-enrolment issuance and consumption had no owner-target restriction and consumption did not recheck created_by authority. Disable and deletion already exclude owner targets; set_role requires an owner actor and excludes owner targets; session revocation is self-scoped. The new regression covers those existing boundaries as well as the re-enrolment Role matrix and stale issuers. OIDC ordinary session creation called mark_asserted without auth_time. The implementation removes that call, requests max_age=0 for explicit elevation, and checks signed auth_time with a 300-second freshness window and at most 30 seconds of clock lead. This follows DESIGN §21 and OpenID Connect Core §§2 and 3.1.2.1 (https://openid.net/specs/openid-connect-core-1_0.html). Decisions for #737: share one Instance budget across passkeys and all OIDC providers: 512 flows, 16 flows per resolved client IP, 16 MiB conservative retained-state reservations, five-minute TTL. Reserve before construction. Evict expired entries in oldest-first order; return 429 at live capacity to preserve in-flight ceremonies. Bound credentials/options and input tokens. Recovery checks share the existing four-worker Argon2 memory bound and fail admission immediately at capacity; cancelled callers retain the permit in blocking work. Cold calternal-auth build is running with four jobs and limited debug data. No gate pass is claimed yet.
Author
Owner

Implementation checkpoint: 597905a35 (job/authfix). Atomic commits cover shared admission primitives, owner re-enrolment, admitted recovery verification, OIDC freshness, finite loopback checks/profile, and ceremony integration.

Observed checks so far:

  • Both required merges: Already up to date.
  • cargo fmt --check exit: 0
  • 48 synthetic SQLite Role-matrix checks passed (issuance, lookup, consumption).
  • Standard-library admission tests: 3 passed, 0 failed (count/IP/bytes, expiry, concurrent reservation, table release).
  • bun install --frozen-lockfile exit: 0
  • bun run build exit: 0 (needed for the server's embedded assets; no UI source changes).

Full Rust gates remain pending the cold dependency build. The auth TCP robustness and local performance round are prepared but have not run. No gate pass is inferred from compilation. The local round is finite and uses synthetic fixtures; it does not reproduce an exploit or resource-exhaustion flood. Exact accounting tests enforce the global memory reservation bound independently of RSS noise.

Freshness detail: store the verified provider authentication instant, not callback receipt time; otherwise a nearly five-minute-old auth_time could grant another five minutes. A regression checks this. The shared cache shrinks historical table allocations as leases finish, so provider churn does not leave uncharged high-water tables.

Implementation checkpoint: 597905a35 (job/authfix). Atomic commits cover shared admission primitives, owner re-enrolment, admitted recovery verification, OIDC freshness, finite loopback checks/profile, and ceremony integration. Observed checks so far: - Both required merges: Already up to date. - cargo fmt --check exit: 0 - 48 synthetic SQLite Role-matrix checks passed (issuance, lookup, consumption). - Standard-library admission tests: 3 passed, 0 failed (count/IP/bytes, expiry, concurrent reservation, table release). - bun install --frozen-lockfile exit: 0 - bun run build exit: 0 (needed for the server's embedded assets; no UI source changes). Full Rust gates remain pending the cold dependency build. The auth TCP robustness and local performance round are prepared but have not run. No gate pass is inferred from compilation. The local round is finite and uses synthetic fixtures; it does not reproduce an exploit or resource-exhaustion flood. Exact accounting tests enforce the global memory reservation bound independently of RSS noise. Freshness detail: store the verified provider authentication instant, not callback receipt time; otherwise a nearly five-minute-old auth_time could grant another five minutes. A regression checks this. The shared cache shrinks historical table allocations as leases finish, so provider churn does not leave uncharged high-water tables.
Author
Owner

Verification progress at head 7dd205861: all requested source changes and regression tests are committed; the working tree is clean. The cold calternal-auth dependency build is still running with CARGO_BUILD_JOBS=4, line-tables-only debug data and incremental builds disabled. Full Rust gates have no result yet. Later checks are ordered as auth validation, one finite local TCP/profile round, then server gates, to make the remaining time useful.

No runtime or performance success is claimed. The three standalone admission tests, 48-case SQLite authority matrix, cargo fmt --check and required web asset build have passed. The final report will distinguish those completed checks from any Rust/runtime checks left incomplete by the three-hour limit.

Verification progress at head 7dd205861: all requested source changes and regression tests are committed; the working tree is clean. The cold calternal-auth dependency build is still running with CARGO_BUILD_JOBS=4, line-tables-only debug data and incremental builds disabled. Full Rust gates have no result yet. Later checks are ordered as auth validation, one finite local TCP/profile round, then server gates, to make the remaining time useful. No runtime or performance success is claimed. The three standalone admission tests, 48-case SQLite authority matrix, cargo fmt --check and required web asset build have passed. The final report will distinguish those completed checks from any Rust/runtime checks left incomplete by the three-hour limit.
Author
Owner

Authfix handoff — verification incomplete at the three-hour limit

Branch: job/authfix. HEAD: a1d4d0c01b. All changes are committed. No push, deploy or promotion was performed. The required merges from origin/dev and job/merge-round-7a both returned Already up to date.

Built

  • #734: re-enrolment issuance, lookup and redemption check the current issuer authority and the owner boundary. Redemption repeats the check atomically. Role matrices and existing owner/session restrictions have regression coverage.
  • #735: explicit OIDC elevation requests max_age=0 and requires signed auth_time within the freshness window. The initiating session records that time, not the callback time. Ordinary OIDC sign-in does not grant fresh authority.
  • #737: passkey and OIDC starts share Instance count, IP and retained-byte admission. Setup grants are checked before state creation. State has ordered expiry; consumption rejects expired state. Inputs, credential lists, unknown-handle attempt state and the auth rate-limit identity map are bounded. New HTTP fixtures and a finite loopback robustness probe exercise the capacity response.
  • #738: known and unknown Users each run one admitted Argon2 recovery verification. A missing row uses a valid dummy hash. The blocking worker retains its permit through request cancellation. The misleading comment is fixed.
  • Added a finite local auth ceremony performance profile. It records latency, CPU, RSS and load, but was not executed.

Files

  • crates/calternal-auth/src/api.rs
  • crates/calternal-auth/src/ceremony.rs
  • crates/calternal-auth/src/lib.rs
  • crates/calternal-auth/src/oidc.rs
  • crates/calternal-auth/src/passkey.rs
  • crates/calternal-auth/src/store.rs
  • tests/adversarial/auth_ceremony.py
  • bench/auth-ceremony.py

Verification
The shared host load reached 162.48 / 160.13 / 147.43. The cold dependency build took most of the time limit. The initial auth lib test build found a typo in the new regression fixture (issue_recovery instead of recovery_codes). Commit a1d4d0c01b fixes it without changing assertions. The corrected revision was not compiled before the cutoff. Do not treat this handoff as passing Rust gates.

Verbatim completed check output:

cargo fmt --check exit: 0
bun install --frozen-lockfile exit: 0
bun run build exit: 0

Standalone std-only ceremony tests (not the crate gate):

running 3 tests
test ceremony::tests::bounds_and_expired_lru_eviction ... ok
test ceremony::tests::cache_allocation_follows_live_leases ... ok
test ceremony::tests::concurrent_admission_never_exceeds_global_cap ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s

The extracted production SQL passed 16 issuance, 16 lookup and 16 consumption authority cases against SQLite. Python syntax and git diff --check passed. These checks do not replace Rust gates.

Verbatim initial auth compile failure (fixed in HEAD, rerun incomplete):

error[E0599]: no method named `issue_recovery` found for struct `store::SqliteAuthStore` in the current scope
error: could not compile `calternal-auth` (lib test) due to 1 previous error

Initial cargo test -p calternal-auth --lib exited 101. Final auth clippy and auth test were stopped with their parent shells at exit 143 for the time limit; this is not a gate result. Auth clippy reached dependency checking. Auth test still reported:

    Blocking waiting for file lock on build directory

Server clippy's earlier queued attempt was stopped while waiting. The final sequential server clippy/test commands and ignored local HTTP round were not reached. Thus calternal-auth and calternal-server clippy/test gates, live robustness evidence, and performance measurements remain required.

Known gaps / next commands
Run per-crate clippy and test for calternal-auth and calternal-server with the job's resource limits. Then run cargo test -p calternal-auth --lib local_auth_robustness_and_performance_round -- --ignored --nocapture --test-threads=1. This executes the finite HTTP probe and writes artifacts/auth-ceremony.json. No live resource-exhaustion flood was run. Synthetic cache tests establish accounting bounds only. The baseline has no auth_ceremony metric; no performance comparison or regression decision is claimed. The supplied per-flow byte values are conservative reservations, not a measured allocator ceiling; runtime verification remains required.

Decisions

  • Shared maximums: 512 flows, 16 per IP, 16 MiB reserved state; 128 KiB per passkey flow, 4 KiB per OIDC flow; 300-second TTL.
  • Evict the oldest expired state, and return 429 at capacity while preserving valid live state. Reads do not extend TTL. Middleware reclaims expiry across all providers before a start.
  • OIDC freshness: five minutes with up to 30 seconds of clock lead, clamped to local time when recording the assertion.
  • Recovery uses the existing four-slot Argon2 budget and returns 429 on saturation.
  • Auth rate-limit state: 8,192 retained identities, 128-byte keys. Reject excess identities while keeping existing counters.
  • Performance profile uses a finite isolated local debug auth router. It is not a production release baseline.

UX gaps closed: None; no UI change.
UX gaps left: No UI change; runtime checks remain incomplete.

Doc comments in all touched modules were reviewed and updated. Logs and review artifacts remain in the worktree; they are not committed. Cleanup completed: cargo clean and removal of apps/web/build and apps/web/.svelte-kit/output.

Verbatim cleanup output:

     Removed 2938 files, 1.3GiB total
cargo clean exit: 0

Working tree is clean.

Authfix handoff — verification incomplete at the three-hour limit Branch: job/authfix. HEAD: a1d4d0c01be2573992480e71e32433bc4782bc01. All changes are committed. No push, deploy or promotion was performed. The required merges from origin/dev and job/merge-round-7a both returned `Already up to date.` Built - #734: re-enrolment issuance, lookup and redemption check the current issuer authority and the owner boundary. Redemption repeats the check atomically. Role matrices and existing owner/session restrictions have regression coverage. - #735: explicit OIDC elevation requests max_age=0 and requires signed auth_time within the freshness window. The initiating session records that time, not the callback time. Ordinary OIDC sign-in does not grant fresh authority. - #737: passkey and OIDC starts share Instance count, IP and retained-byte admission. Setup grants are checked before state creation. State has ordered expiry; consumption rejects expired state. Inputs, credential lists, unknown-handle attempt state and the auth rate-limit identity map are bounded. New HTTP fixtures and a finite loopback robustness probe exercise the capacity response. - #738: known and unknown Users each run one admitted Argon2 recovery verification. A missing row uses a valid dummy hash. The blocking worker retains its permit through request cancellation. The misleading comment is fixed. - Added a finite local auth ceremony performance profile. It records latency, CPU, RSS and load, but was not executed. Files - crates/calternal-auth/src/api.rs - crates/calternal-auth/src/ceremony.rs - crates/calternal-auth/src/lib.rs - crates/calternal-auth/src/oidc.rs - crates/calternal-auth/src/passkey.rs - crates/calternal-auth/src/store.rs - tests/adversarial/auth_ceremony.py - bench/auth-ceremony.py Verification The shared host load reached 162.48 / 160.13 / 147.43. The cold dependency build took most of the time limit. The initial auth lib test build found a typo in the new regression fixture (issue_recovery instead of recovery_codes). Commit a1d4d0c01be2573992480e71e32433bc4782bc01 fixes it without changing assertions. The corrected revision was not compiled before the cutoff. Do not treat this handoff as passing Rust gates. Verbatim completed check output: ``` cargo fmt --check exit: 0 bun install --frozen-lockfile exit: 0 bun run build exit: 0 ``` Standalone std-only ceremony tests (not the crate gate): ``` running 3 tests test ceremony::tests::bounds_and_expired_lru_eviction ... ok test ceremony::tests::cache_allocation_follows_live_leases ... ok test ceremony::tests::concurrent_admission_never_exceeds_global_cap ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s ``` The extracted production SQL passed 16 issuance, 16 lookup and 16 consumption authority cases against SQLite. Python syntax and git diff --check passed. These checks do not replace Rust gates. Verbatim initial auth compile failure (fixed in HEAD, rerun incomplete): ``` error[E0599]: no method named `issue_recovery` found for struct `store::SqliteAuthStore` in the current scope error: could not compile `calternal-auth` (lib test) due to 1 previous error ``` Initial cargo test -p calternal-auth --lib exited 101. Final auth clippy and auth test were stopped with their parent shells at exit 143 for the time limit; this is not a gate result. Auth clippy reached dependency checking. Auth test still reported: ``` Blocking waiting for file lock on build directory ``` Server clippy's earlier queued attempt was stopped while waiting. The final sequential server clippy/test commands and ignored local HTTP round were not reached. Thus calternal-auth and calternal-server clippy/test gates, live robustness evidence, and performance measurements remain required. Known gaps / next commands Run per-crate clippy and test for calternal-auth and calternal-server with the job's resource limits. Then run `cargo test -p calternal-auth --lib local_auth_robustness_and_performance_round -- --ignored --nocapture --test-threads=1`. This executes the finite HTTP probe and writes artifacts/auth-ceremony.json. No live resource-exhaustion flood was run. Synthetic cache tests establish accounting bounds only. The baseline has no auth_ceremony metric; no performance comparison or regression decision is claimed. The supplied per-flow byte values are conservative reservations, not a measured allocator ceiling; runtime verification remains required. Decisions - Shared maximums: 512 flows, 16 per IP, 16 MiB reserved state; 128 KiB per passkey flow, 4 KiB per OIDC flow; 300-second TTL. - Evict the oldest expired state, and return 429 at capacity while preserving valid live state. Reads do not extend TTL. Middleware reclaims expiry across all providers before a start. - OIDC freshness: five minutes with up to 30 seconds of clock lead, clamped to local time when recording the assertion. - Recovery uses the existing four-slot Argon2 budget and returns 429 on saturation. - Auth rate-limit state: 8,192 retained identities, 128-byte keys. Reject excess identities while keeping existing counters. - Performance profile uses a finite isolated local debug auth router. It is not a production release baseline. UX gaps closed: None; no UI change. UX gaps left: No UI change; runtime checks remain incomplete. Doc comments in all touched modules were reviewed and updated. Logs and review artifacts remain in the worktree; they are not committed. Cleanup completed: cargo clean and removal of apps/web/build and apps/web/.svelte-kit/output. Verbatim cleanup output: ``` Removed 2938 files, 1.3GiB total cargo clean exit: 0 ``` Working tree is clean.
Author
Owner

Independent review of job/authfix — #734

Verdict: request changes. Two branch regressions need fixes.

Scope and method

Reviewed head: a1d4d0c01be2573992480e71e32433bc4782bc01.
Review branch: job/rev2-authfix.
Review report head: bc9e2553b72f5116f9865657840632af812f6259.
Review base: 440e19dce23040ac8ebaae88f0469b6535b1afcb.

Read CLAUDE.md, CONTEXT.md, DESIGN §§7 and 21, and issues #734, #735, #737
and #738. Also checked the target's DESIGN §58. It describes agent discovery
and setup, not interactive performance. Used the performance rules in
CLAUDE.md and the job prompt for the source review.

Inspected git diff origin/dev...a1d4d0c01. Its merge base is
c4a61e8cf090170f35b1bed3350d9de20c83ecd5. The diff includes earlier merged
jobs. Isolated the authfix commits from 384607a69 through the reviewed head
to assign findings to this branch. Read supporting code in the author's
worktree without changing it. Did not use the author's report as evidence.
Fetched origin once; origin/dev remained at the review base. No merge was
needed for this source-only job, which has no final build gates.

Findings, in severity order

R1 — P1: OIDC-only Users have no Settings path to fresh authentication

Changed code: crates/calternal-auth/src/api.rs:1966.
Supporting code: apps/web/src/routes/settings/api.svelte.ts:30 and
apps/web/src/routes/settings/account/PasskeysGroup.svelte:73.

Ordinary OIDC sign-in now correctly leaves the local session without fresh
authority. However, the shared Settings step-up helper responds to 403 only
with a passkey assertion. There is no OIDC re-authentication call under
apps/web/src. A User with no passkey cannot complete that assertion or add
their first passkey. Other account changes and Admin changes use this helper.
Signing in again no longer gives a short period in which those actions work.

Fix: extend the existing shared helper to use a linked OIDC provider when the
User has no passkeys. Request explicit provider re-authentication, retain the
initiating local session, and resume the action after the callback. Keep the
new signed freshness checks and ordinary sign-in behaviour. Update the
helper's passkey-only comments and cancellation messages.

Regression: an OIDC-only User can add their first passkey from Settings only
after explicit re-authentication. An OIDC-only admin can complete an Admin
change. Cancellation leaves the action unapplied. Another local session
remains without fresh authority. These tests must use the Settings flow;
the new backend tests call the re-authentication route directly and miss this
gap.

Tracking: #734, related to #735. The existing-issue search for "OIDC" found
no separate issue for this regression. Do not create a second branch issue.

R2 — P2: accepted key sets can exceed the new assertion limits

Changed code: crates/calternal-auth/src/passkey.rs:166 and :489.
Supporting code: crates/calternal-auth/src/store.rs:2667 and
crates/calternal-auth/src/api.rs:1298.

Registration allows 64 existing keys and then inserts another key. Completion
does not enforce a count limit. Assertion refuses more than 64 keys. Key
removal starts through that same assertion path, so the User cannot remove a
key to reduce the list. Concurrent registration starts can pass admission
before either completion adds a key.

The byte checks also differ: registration counts credential ID bytes;
assertion counts credential JSON bytes. Each uses a 32 KiB boundary. Thus a
successful registration does not prove the resulting key set is supported
by assertion. Recovery and re-enrolment also fail to start after the existing
key set exceeds registration's bounds. Ordinary discoverable sign-in still
works; this finding does not claim a complete sign-in failure.

Fix: define one supported-key invariant and enforce it atomically at every
credential insertion, including concurrent completions. Include the new key
in the decision. Keep bounded assertion and removal available for existing
larger key sets, so Users can reduce them. Reuse one definition of count and
byte limits instead of separate checks with different inputs.

Regression: cover the count boundary, concurrent additions near it, the JSON
byte boundary, and removal from a pre-existing larger key set. Every accepted
key set must remain usable for assertion and key removal.

Tracking: #734, related to #737. Searches for passkey, ceremony and
passkey limit found no separate issue for this regression. Both limits
share one fix and one owner.

Other review results

  • Owner authority: issuance, target lookup and consumption check current
    issuer and target state. An admin cannot recover an owner. Consumption and
    credential insertion share a transaction, so a failed insertion does not
    consume the grant. Old grants receive the same current-authority check.
  • OIDC: signed auth_time is checked after token validation. A missing or
    stale value fails. Clock lead is limited to 30 seconds. The stored instant
    is the provider authentication time, capped at local time. The identity and
    initiating live-session checks remain. Ordinary sign-in no longer grants
    fresh authority.
  • Ceremony state: count, reserved bytes and per-IP admission share a budget.
    Pending state consumes a lease; cancellation before publication releases
    it. Expiry and consumption remove ordering metadata and release capacity.
    Live entries are not removed to admit other requests. Setup grant checks
    occur before challenge construction.
  • Recovery: one admitted blocking verification runs for both known and
    unknown Users. The result is combined with row presence after verification.
    The blocking worker retains its permit through request cancellation.
  • Isolation: the changed routes retain User and session bindings. The new
    re-enrolment SQL checks both issuer and target in the transaction. No new
    cross-User write or data-corruption defect was found in this scope.
  • Reuse: searched existing auth helpers with rg. The branch shares admission
    and pending expiry rather than adding one implementation per ceremony.
  • Comments: the new module comments explain budget, cancellation, expiry and
    authority invariants. R1 identifies a supporting comment that the behaviour
    change makes incomplete. R2 identifies an invariant absent at insertion.
  • Test history: read git log -p for the changed auth files. Existing test
    assertions were not weakened in the authfix commits. Existing API test
    calls gained the IP argument; the legacy recovery fixture call was corrected.
    New Role, freshness, verifier-count and capacity assertions address the old
    defects. They do not cover R1 or R2. Tests were not executed.
  • Performance: ordered expiry avoids scanning all live ceremonies at every
    insertion. Rate-limit maps still scan up to their fixed caps per request.
    The new profile covers login-start latency, CPU and RSS for one IP, with
    16 pending flows. It does not measure full Instance capacity, OIDC starts,
    or recovery. No measured performance claim is made by this review.

Delivery and limits

Built: source review documents only. No product code changed.
Files: audit-findings.md and review-authfix.md.
Gate output: none. The LIGHT job explicitly prohibits builds and tests.
No servers, browsers, adversarial requests, dependency changes or build output.
No cleanup command was needed. No pushes, deploys or merges.

Known gaps: findings need implementation and runtime regression checks in a
build job. Performance and UI behaviour were reviewed from source only.
No defects outside this branch were filed.

Decisions: used the authfix commit range to separate this job from earlier
merged work in the requested diff. Used the stated performance rules because
the cited DESIGN §58 has a different subject. No product design decisions.

UX gaps closed: none; review only.
UX gaps left: R1 and R2.

# Independent review of job/authfix — #734 Verdict: request changes. Two branch regressions need fixes. ## Scope and method Reviewed head: `a1d4d0c01be2573992480e71e32433bc4782bc01`. Review branch: `job/rev2-authfix`. Review report head: `bc9e2553b72f5116f9865657840632af812f6259`. Review base: `440e19dce23040ac8ebaae88f0469b6535b1afcb`. Read CLAUDE.md, CONTEXT.md, DESIGN §§7 and 21, and issues #734, #735, #737 and #738. Also checked the target's DESIGN §58. It describes agent discovery and setup, not interactive performance. Used the performance rules in CLAUDE.md and the job prompt for the source review. Inspected `git diff origin/dev...a1d4d0c01`. Its merge base is `c4a61e8cf090170f35b1bed3350d9de20c83ecd5`. The diff includes earlier merged jobs. Isolated the authfix commits from `384607a69` through the reviewed head to assign findings to this branch. Read supporting code in the author's worktree without changing it. Did not use the author's report as evidence. Fetched origin once; origin/dev remained at the review base. No merge was needed for this source-only job, which has no final build gates. ## Findings, in severity order ### R1 — P1: OIDC-only Users have no Settings path to fresh authentication Changed code: `crates/calternal-auth/src/api.rs:1966`. Supporting code: `apps/web/src/routes/settings/api.svelte.ts:30` and `apps/web/src/routes/settings/account/PasskeysGroup.svelte:73`. Ordinary OIDC sign-in now correctly leaves the local session without fresh authority. However, the shared Settings step-up helper responds to 403 only with a passkey assertion. There is no OIDC re-authentication call under `apps/web/src`. A User with no passkey cannot complete that assertion or add their first passkey. Other account changes and Admin changes use this helper. Signing in again no longer gives a short period in which those actions work. Fix: extend the existing shared helper to use a linked OIDC provider when the User has no passkeys. Request explicit provider re-authentication, retain the initiating local session, and resume the action after the callback. Keep the new signed freshness checks and ordinary sign-in behaviour. Update the helper's passkey-only comments and cancellation messages. Regression: an OIDC-only User can add their first passkey from Settings only after explicit re-authentication. An OIDC-only admin can complete an Admin change. Cancellation leaves the action unapplied. Another local session remains without fresh authority. These tests must use the Settings flow; the new backend tests call the re-authentication route directly and miss this gap. Tracking: #734, related to #735. The existing-issue search for `"OIDC"` found no separate issue for this regression. Do not create a second branch issue. ### R2 — P2: accepted key sets can exceed the new assertion limits Changed code: `crates/calternal-auth/src/passkey.rs:166` and `:489`. Supporting code: `crates/calternal-auth/src/store.rs:2667` and `crates/calternal-auth/src/api.rs:1298`. Registration allows 64 existing keys and then inserts another key. Completion does not enforce a count limit. Assertion refuses more than 64 keys. Key removal starts through that same assertion path, so the User cannot remove a key to reduce the list. Concurrent registration starts can pass admission before either completion adds a key. The byte checks also differ: registration counts credential ID bytes; assertion counts credential JSON bytes. Each uses a 32 KiB boundary. Thus a successful registration does not prove the resulting key set is supported by assertion. Recovery and re-enrolment also fail to start after the existing key set exceeds registration's bounds. Ordinary discoverable sign-in still works; this finding does not claim a complete sign-in failure. Fix: define one supported-key invariant and enforce it atomically at every credential insertion, including concurrent completions. Include the new key in the decision. Keep bounded assertion and removal available for existing larger key sets, so Users can reduce them. Reuse one definition of count and byte limits instead of separate checks with different inputs. Regression: cover the count boundary, concurrent additions near it, the JSON byte boundary, and removal from a pre-existing larger key set. Every accepted key set must remain usable for assertion and key removal. Tracking: #734, related to #737. Searches for `passkey`, `ceremony` and `passkey limit` found no separate issue for this regression. Both limits share one fix and one owner. ## Other review results - Owner authority: issuance, target lookup and consumption check current issuer and target state. An admin cannot recover an owner. Consumption and credential insertion share a transaction, so a failed insertion does not consume the grant. Old grants receive the same current-authority check. - OIDC: signed `auth_time` is checked after token validation. A missing or stale value fails. Clock lead is limited to 30 seconds. The stored instant is the provider authentication time, capped at local time. The identity and initiating live-session checks remain. Ordinary sign-in no longer grants fresh authority. - Ceremony state: count, reserved bytes and per-IP admission share a budget. Pending state consumes a lease; cancellation before publication releases it. Expiry and consumption remove ordering metadata and release capacity. Live entries are not removed to admit other requests. Setup grant checks occur before challenge construction. - Recovery: one admitted blocking verification runs for both known and unknown Users. The result is combined with row presence after verification. The blocking worker retains its permit through request cancellation. - Isolation: the changed routes retain User and session bindings. The new re-enrolment SQL checks both issuer and target in the transaction. No new cross-User write or data-corruption defect was found in this scope. - Reuse: searched existing auth helpers with `rg`. The branch shares admission and pending expiry rather than adding one implementation per ceremony. - Comments: the new module comments explain budget, cancellation, expiry and authority invariants. R1 identifies a supporting comment that the behaviour change makes incomplete. R2 identifies an invariant absent at insertion. - Test history: read `git log -p` for the changed auth files. Existing test assertions were not weakened in the authfix commits. Existing API test calls gained the IP argument; the legacy recovery fixture call was corrected. New Role, freshness, verifier-count and capacity assertions address the old defects. They do not cover R1 or R2. Tests were not executed. - Performance: ordered expiry avoids scanning all live ceremonies at every insertion. Rate-limit maps still scan up to their fixed caps per request. The new profile covers login-start latency, CPU and RSS for one IP, with 16 pending flows. It does not measure full Instance capacity, OIDC starts, or recovery. No measured performance claim is made by this review. ## Delivery and limits Built: source review documents only. No product code changed. Files: `audit-findings.md` and `review-authfix.md`. Gate output: none. The LIGHT job explicitly prohibits builds and tests. No servers, browsers, adversarial requests, dependency changes or build output. No cleanup command was needed. No pushes, deploys or merges. Known gaps: findings need implementation and runtime regression checks in a build job. Performance and UI behaviour were reviewed from source only. No defects outside this branch were filed. Decisions: used the authfix commit range to separate this job from earlier merged work in the requested diff. Used the stated performance rules because the cited DESIGN §58 has a different subject. No product design decisions. UX gaps closed: none; review only. UX gaps left: R1 and R2.
Author
Owner

Started review corrections on job/authfix, head a1d4d0c01be2573992480e71e32433bc4782bc01.
Read review-authfix.md: addressing R1 (OIDC-only Settings step-up) and R2 (credential insertion limits) with regression tests before gates.
Host load average is 54.86, 48.11, 51.57. Latest verification policy reserves full authz/admin/adversarial matrices and non-performance-issue measurements for the merge round. Focused regression checks will run here; compilation is limited to four jobs.

Started review corrections on `job/authfix`, head `a1d4d0c01be2573992480e71e32433bc4782bc01`. Read review-authfix.md: addressing R1 (OIDC-only Settings step-up) and R2 (credential insertion limits) with regression tests before gates. Host load average is 54.86, 48.11, 51.57. Latest verification policy reserves full authz/admin/adversarial matrices and non-performance-issue measurements for the merge round. Focused regression checks will run here; compilation is limited to four jobs.
Author
Owner

R1: the shared Settings helper still attempted only a passkey assertion after HTTP 403. Added linked-provider re-authentication for Users without passkeys. The existing Settings form now has a regression for adding the first key and cancelling. Shared helper tests also cover callback failure and closing the provider window. An explicit Continue action opens the provider window under browser activation; the initiating page and action remain in memory.

R2: registration preflight counted credential IDs but assertion counted JSON. All credential insertion paths now call one transaction helper that includes the new key in the count and UTF-8 byte decision. Oversized existing sets use discoverable assertion with the original User/session checks, keeping removal available. Added concurrent last-slot, exact byte-boundary, grant rollback and legacy-removal regressions.

Merged origin/dev once at c4faf184d. Current web focused tests: Test Files 3 passed (3) / Tests 11 passed (11); the first-passkey Settings component: Test Files 1 passed (1) / Tests 2 passed (2). Rust dependency compilation is still running with four jobs.

R1: the shared Settings helper still attempted only a passkey assertion after HTTP 403. Added linked-provider re-authentication for Users without passkeys. The existing Settings form now has a regression for adding the first key and cancelling. Shared helper tests also cover callback failure and closing the provider window. An explicit Continue action opens the provider window under browser activation; the initiating page and action remain in memory. R2: registration preflight counted credential IDs but assertion counted JSON. All credential insertion paths now call one transaction helper that includes the new key in the count and UTF-8 byte decision. Oversized existing sets use discoverable assertion with the original User/session checks, keeping removal available. Added concurrent last-slot, exact byte-boundary, grant rollback and legacy-removal regressions. Merged origin/dev once at c4faf184d. Current web focused tests: `Test Files 3 passed (3)` / `Tests 11 passed (11)`; the first-passkey Settings component: `Test Files 1 passed (1)` / `Tests 2 passed (2)`. Rust dependency compilation is still running with four jobs.
Author
Owner

Web slice committed as 6d65bbea1; test-module documentation as fafef0d15.

Auth clippy passed:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 39m 50s

All new insertion, exact valid-JSON boundary, legacy-removal, callback-hint and signed-freshness regressions passed in the auth test run. One existing App Password queued-revocation test returned Unavailable at its post-revocation verification under the parallel suite. Its assertions are unchanged; checking it in isolation next.

test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 67.97s

The focused Python probe ran against the production auth router over live loopback TCP. Performance was not run.

PASS: 22 bounded local requests; invalid setup/JSON and oversized recovery refused; 16 admitted flows; capacity HTTP 429
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 0.67s

Server gates are now compiling. A dated prebuilt server was incompatible with the current Appearance API, so browser captures still need the current server binary. No screenshot or server gate pass is claimed yet.

Web slice committed as 6d65bbea1; test-module documentation as fafef0d15. Auth clippy passed: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 39m 50s ``` All new insertion, exact valid-JSON boundary, legacy-removal, callback-hint and signed-freshness regressions passed in the auth test run. One existing App Password queued-revocation test returned `Unavailable` at its post-revocation verification under the parallel suite. Its assertions are unchanged; checking it in isolation next. ``` test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 67.97s ``` The focused Python probe ran against the production auth router over live loopback TCP. Performance was not run. ``` PASS: 22 bounded local requests; invalid setup/JSON and oversized recovery refused; 16 admitted flows; capacity HTTP 429 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 0.67s ``` Server gates are now compiling. A dated prebuilt server was incompatible with the current Appearance API, so browser captures still need the current server binary. No screenshot or server gate pass is claimed yet.
Author
Owner

The full auth test run found an existing App Password regression, app_password_revoke_rejects_queued_verification. It also fails in isolation (1.05 s), so this is not a SLOW-only host finding. The follower can run during the revocation transaction and return Unavailable before entering the credential's single-flight queue. Its existing assertion requires rejection of the revoked credential. I moved the mutation check behind that queue; the existing test and all its assertions remain unchanged. Verification of this correction is queued behind the server crate gates.

Saved review fixes: 5b761a337 publishes a correlated callback completion hint for COOP-isolated OIDC windows; baf171f79 enforces the shared supported key-set bounds atomically and keeps legacy larger sets usable for verification/removal. No dependency or migration changes.

The full auth test run found an existing App Password regression, `app_password_revoke_rejects_queued_verification`. It also fails in isolation (1.05 s), so this is not a SLOW-only host finding. The follower can run during the revocation transaction and return `Unavailable` before entering the credential's single-flight queue. Its existing assertion requires rejection of the revoked credential. I moved the mutation check behind that queue; the existing test and all its assertions remain unchanged. Verification of this correction is queued behind the server crate gates. Saved review fixes: `5b761a337` publishes a correlated callback completion hint for COOP-isolated OIDC windows; `baf171f79` enforces the shared supported key-set bounds atomically and keeps legacy larger sets usable for verification/removal. No dependency or migration changes.
Author
Owner

The queued App Password revocation regression now passes with the original assertions:

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.89s

Saved as 21189fac4. The final auth clippy/test run is queued behind the server test build. Server clippy passed:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s

The browser fixture required two corrections: remove the setup credential from the virtual device before registering a second device, and match the provider form's return by pathname (an empty query must not bypass the callback fixture). A reduced diagnostic completed the isolated provider callback and a real passkey write. The complete first-key and Admin screenshot/regression run is in progress. It uses this branch's production SPA with a freshly built compatible prebuilt server; the OIDC provider leg is fixture-only. Rust tests independently verify signed provider claims and session binding.

The supplied benchmark measures ceremony-start requests and has no auth-ceremony baseline. It cannot establish sign-in Argon2 p95. Performance and the full matrices remain for the merge round under the latest verification policy.

The queued App Password revocation regression now passes with the original assertions: ``` test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.89s ``` Saved as `21189fac4`. The final auth clippy/test run is queued behind the server test build. Server clippy passed: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s ``` The browser fixture required two corrections: remove the setup credential from the virtual device before registering a second device, and match the provider form's return by pathname (an empty query must not bypass the callback fixture). A reduced diagnostic completed the isolated provider callback and a real passkey write. The complete first-key and Admin screenshot/regression run is in progress. It uses this branch's production SPA with a freshly built compatible prebuilt server; the OIDC provider leg is fixture-only. Rust tests independently verify signed provider claims and session binding. The supplied benchmark measures ceremony-start requests and has no auth-ceremony baseline. It cannot establish sign-in Argon2 p95. Performance and the full matrices remain for the merge round under the latest verification policy.
Author
Owner

Final production screenshot set for #734 R1, head ade052781.

Mac platform is emulated at 390, 820 and 1440 px, in Light and Dark. The provider leg is a test-only cross-origin COOP fixture. The production server owns the User, passkey and invitation writes. The real provider signature and session binding are covered by Rust tests. Use the links below for review; earlier uploads are not the final set.

PASS: Settings first-key and Admin invitation flows; six Mac width/theme pairs each; pointer, touch and keyboard; cancellation unapplied; COOP callbacks resume real writes
Width / theme First key Account check Invitations Admin check
390 / light Image Image Image Image
390 / dark Image Image Image Image
820 / light Image Image Image Image
820 / dark Image Image Image Image
1440 / light Image Image Image Image
1440 / dark Image Image Image Image
Final production screenshot set for #734 R1, head `ade052781`. Mac platform is emulated at 390, 820 and 1440 px, in Light and Dark. The provider leg is a test-only cross-origin COOP fixture. The production server owns the User, passkey and invitation writes. The real provider signature and session binding are covered by Rust tests. Use the links below for review; earlier uploads are not the final set. ``` PASS: Settings first-key and Admin invitation flows; six Mac width/theme pairs each; pointer, touch and keyboard; cancellation unapplied; COOP callbacks resume real writes ``` | Width / theme | First key | Account check | Invitations | Admin check | |---|---|---|---|---| | 390 / light | [Image](https://git.kayg.org/attachments/b7690034-ac68-4493-9419-0abc94716b71) | [Image](https://git.kayg.org/attachments/c9024a83-b2af-43b1-bc8c-8f3e9c94b1a8) | [Image](https://git.kayg.org/attachments/ec38b13f-422f-4122-9275-892eb7df1d6f) | [Image](https://git.kayg.org/attachments/12cdef56-05d3-40e8-b032-3a4f81a07565) | | 390 / dark | [Image](https://git.kayg.org/attachments/30ae9e5f-3b36-48d2-b586-3d22be3f7f37) | [Image](https://git.kayg.org/attachments/ed10204f-6cfa-4ef9-8643-62b540e90efe) | [Image](https://git.kayg.org/attachments/b246aa4d-018b-41e5-97fb-80e222c43372) | [Image](https://git.kayg.org/attachments/6ba78eda-fdbb-447b-8f1f-b735bb64c766) | | 820 / light | [Image](https://git.kayg.org/attachments/44ba84c3-2107-4610-b6bb-27255e8a8cb3) | [Image](https://git.kayg.org/attachments/a3be3535-1f01-4059-a8a8-20183855720a) | [Image](https://git.kayg.org/attachments/e9dcec1c-3af2-4fe4-b658-9e9c7163bca2) | [Image](https://git.kayg.org/attachments/442ba348-9cb7-47e6-bcc5-60f365d4c405) | | 820 / dark | [Image](https://git.kayg.org/attachments/19927f42-05a1-4ae9-a511-01869b773368) | [Image](https://git.kayg.org/attachments/f79d7d41-17b6-420e-91cc-8d50b84230ba) | [Image](https://git.kayg.org/attachments/1f076e3e-453a-4574-b542-eb75bd67eff0) | [Image](https://git.kayg.org/attachments/1a9e9ff0-0a2d-4415-82cc-857cbff593ae) | | 1440 / light | [Image](https://git.kayg.org/attachments/c16f8952-8734-48b7-b372-35fd33c4a4bf) | [Image](https://git.kayg.org/attachments/7c821840-53f9-439c-bde2-e1dfb934a990) | [Image](https://git.kayg.org/attachments/860ef028-7e71-4aca-bcad-fe81310341b4) | [Image](https://git.kayg.org/attachments/7cede972-ba6d-4709-a8c2-720b9a74116d) | | 1440 / dark | [Image](https://git.kayg.org/attachments/f6328c19-7155-4194-bd3f-821653d58c4f) | [Image](https://git.kayg.org/attachments/40b365b7-6f76-4bff-8d16-9fc6a628d6c8) | [Image](https://git.kayg.org/attachments/676e1517-bea2-4a73-958f-90d4fc77230a) | [Image](https://git.kayg.org/attachments/4dfbc1df-72f2-4721-8386-0e2b5c1b49c8) |
Author
Owner

Head ade052781, worktree clean. The final production browser regression passed; all 24 final Mac-emulated screenshots are attached and linked in the previous comment.

Server gates passed:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s
    Finished `test` profile [unoptimized + debuginfo] target(s) in 30m 23s
test result: ok. 161 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 16.53s

The final auth clippy/test run is now active after waiting for that build. No further source changes are planned unless a gate finds a defect. Build cleanup follows those gates.

Head `ade052781`, worktree clean. The final production browser regression passed; all 24 final Mac-emulated screenshots are attached and linked in the previous comment. Server gates passed: ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s Finished `test` profile [unoptimized + debuginfo] target(s) in 30m 23s test result: ok. 161 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 16.53s ``` The final auth clippy/test run is now active after waiting for that build. No further source changes are planned unless a gate finds a defect. Build cleanup follows those gates.
Author
Owner

Final report for #734. Branch job/authfix, head 280e12d42b5a3c6f3dd2eaa31d487bc400ae41c4.

Built

  • R1: Settings uses a linked, configured provider when the User has no passkey. The existing confirmation sheet opens the provider window from User activation. The initiating form stays in memory. A correlated completion cookie handles COOP isolation. It grants no authority; the server checks the retried action. Cancel leaves the action unapplied.
  • R2: every credential insertion checks the resulting set in one SQL statement: at most 64 keys and 32 KiB of credential IDs plus UTF-8 key JSON. Failed insertions roll back setup, invitation and recovery grants. Larger legacy sets use discoverable authentication. Verification and removal still require the correct User, user verification and initiating live session.
  • Fixed the existing App Password queued-revocation race. A follower now joins the credential queue before checking mutation contention. All existing test assertions are unchanged.
  • Added a focused live-loopback auth probe runner and production-browser regression. The browser test covers real passkey and invitation writes, cancellation, touch, keyboard and 36 Mac-emulated review images. The OIDC-only read metadata and provider start/return are test fixtures; signed provider claims and session binding are tested in Rust.

Files

apps/web/e2e/authfix-settings.mjs
apps/web/src/lib/components/ConfirmSheet.svelte
apps/web/src/routes/settings/[...path]/+page.svelte
apps/web/src/routes/settings/account/AppPasswordsGroup.svelte
apps/web/src/routes/settings/account/PasskeysGroup.svelte
apps/web/src/routes/settings/account/PasskeysGroup.svelte.test.ts
apps/web/src/routes/settings/account/ProfileGroup.svelte
apps/web/src/routes/settings/account/RecoveryKeySheet.svelte
apps/web/src/routes/settings/account/SessionsGroup.svelte
apps/web/src/routes/settings/account/SignInMethodsGroup.svelte
apps/web/src/routes/settings/admin/ConfigGroup.svelte
apps/web/src/routes/settings/admin/InvitationsGroup.svelte
apps/web/src/routes/settings/admin/InvitationsGroup.svelte.test.ts
apps/web/src/routes/settings/admin/UsersGroup.svelte
apps/web/src/routes/settings/api.svelte.test.ts
apps/web/src/routes/settings/api.svelte.ts
crates/calternal-auth/src/api.rs
crates/calternal-auth/src/ceremony.rs
crates/calternal-auth/src/passkey.rs
crates/calternal-auth/src/store.rs

Final gates — verbatim summary lines

cargo fmt --check: exit 0, no output.

cargo clippy -p calternal-auth --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 33m 03s

RUST_TEST_THREADS=2 cargo test -p calternal-auth

    Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 32s
test result: ok. 103 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 283.60s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

cargo clippy -p calternal-server --all-targets -- -D warnings

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s

cargo test -p calternal-server

    Finished `test` profile [unoptimized + debuginfo] target(s) in 30m 23s
test result: ok. 161 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 16.53s

bun run check

svelte-check found 0 errors and 0 warnings

bunx vitest run src/routes/settings/api.svelte.test.ts src/routes/settings/account/PasskeysGroup.svelte.test.ts src/routes/settings/admin/InvitationsGroup.svelte.test.ts src/lib/auth/passkeys.test.ts src/routes/settings/shared-components.guard.test.ts --maxWorkers=2 (from apps/web)

 Test Files  5 passed (5)
      Tests  17 passed (17)
   Start at  20:31:14
   Duration  17.31s (transform 59%, environment 18%, setup 10%, tests 7%, import 5%)

Focused live auth router over TCP:

test api::tests::local_auth_ceremony_probe_tcp ... PASS: 22 bounded local requests; invalid setup/JSON and oversized recovery refused; 16 admitted flows; capacity HTTP 429
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 0.67s

Production browser:

PASS: Settings first-key and Admin invitation flows; six Mac width/theme pairs across changed screens; pointer, touch and keyboard; cancellation unapplied; COOP callbacks resume real writes

Failures resolved and diagnostic checks — verbatim summaries

The first auth suite failed the queued App Password test. It also failed in isolation. The code fix passed its focused regression. The next suite failed while opening the signed OIDC test database, before its authentication case. That test passed in isolation. The final full auth suite passed with two test threads. No OIDC assertion or production timeout was changed.

r2-capacity

    Finished `test` profile [unoptimized + debuginfo] target(s) in 29m 33s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 103 filtered out; finished in 0.12s

r2-legacy

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 103 filtered out; finished in 1.52s

auth-clippy

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 39m 50s

auth-test

    Finished `test` profile [unoptimized + debuginfo] target(s) in 3m 19s
test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 67.97s

auth-revoke-focused

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.05s

auth-revoke-fixed

    Finished `test` profile [unoptimized + debuginfo] target(s) in 8m 21s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.89s

auth-test-final

    Finished `test` profile [unoptimized + debuginfo] target(s) in 7m 58s
test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 71.98s

auth-oidc-focused

    Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 30s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 54.02s

UX gaps closed

  • First-passkey and Admin forms can use a linked provider. Passkey-only text no longer describes these checks.
  • Cancel and Escape keep actions unapplied. Provider isolation does not lose the initiating form. A forged completion hint cannot grant authority.
  • The browser regression proves real writes before screenshot capture can spend the freshness window. Touch cancellation writes no key.
  • Error diagnostics redact temporary credential headers.

UX gaps left / known gaps

  • #945 tracks the full-key-set error text. It uses the existing HTTP 429 wait message, although the User must remove a key to free capacity. This is a non-blocking text gap.
  • COOP can prevent the parent from closing the provider completion window. The original action resumes through its correlated hint. Actual-provider focus and Apple-client behavior still need the merge-round check.
  • No sign-in p95 claim is made. The supplied benchmark measures ceremony starts and explicitly has no auth-ceremony baseline. It does not measure Argon2 sign-in work.
  • Full web suites, full cross-User/Admin matrices, release builds and real Mac interoperability were not run, as required by the latest verification policy.

Decisions

  • Choose the first configured provider that is linked to the User. No new provider picker is added.
  • Reuse the existing confirmation sheet and request helper. Open the provider window on explicit Continue so browsers retain User activation.
  • Use a five-minute, readable, digest-only completion hint for isolated windows. Keep the callback redirect contract and server authority checks.
  • Use the existing 64-key ceremony bound and one quarter of the 128 KiB reservation for inserted key-set bytes. Retain the existing 1024-byte credential-ID bound. Use discovery for larger legacy sets.

For the merge round

  • XUSER_MATRIX_ONLY=1 bash tests/adversarial/run.sh: prove auth routes cannot cross User boundaries, including invalid and stale sessions.
  • AUTHZ_MATRIX_ONLY=1 ADMIN_DENIAL_ONLY=1 bash tests/adversarial/run.sh: prove denied Admin mutations stay unapplied. Run the full authz matrix once on the combined branch as well.
  • cd apps/web && bun run test and bun run test:e2e:auth: check the combined web changes and complete auth browser flows.
  • On the perf VM, with a prepared auth router and a shared build: flock /root/perf.lock bash -c 'uptime; python3 bench/auth-ceremony.py --url "$AUTH_URL" --pid "$AUTH_PID" --output artifacts/auth-ceremony.json'. This proves the bounded start profile only. A representative Argon2 sign-in profile and baseline are still needed to judge p95 noise.
  • Hold ~/.local/state/codex-jobs/calternal/macvm.lock for real Mac checks: flock -w 7200 ~/.local/state/codex-jobs/calternal/macvm.lock netbird ssh --no-browser calternal@10.69.69.21. Check an actual configured provider in Safari: callback completion, parent focus, first-key creation, Admin mutation, cancellation and stale-session refusal. The owner must complete any credential prompts.

Cleanup

Fetched and merged origin/dev once (c4faf184d, merge 0bd0a05fd). No push or deployment. Worktree is clean. Cargo cleanup:

     Removed 15119 files, 6.6GiB total

Web build output and temporary test data are deleted. Screenshots remain in ignored artifacts/authfix-settings/; no review image is committed.

Final screenshots

Use this set for review. It matches the final browser regression. Earlier image links remain historical evidence.

Width / theme First key Account check Invitations Admin check Users Recovery intro
390 / light Image Image Image Image Image Image
390 / dark Image Image Image Image Image Image
820 / light Image Image Image Image Image Image
820 / dark Image Image Image Image Image Image
1440 / light Image Image Image Image Image Image
1440 / dark Image Image Image Image Image Image
Final report for #734. Branch `job/authfix`, head `280e12d42b5a3c6f3dd2eaa31d487bc400ae41c4`. **Built** - R1: Settings uses a linked, configured provider when the User has no passkey. The existing confirmation sheet opens the provider window from User activation. The initiating form stays in memory. A correlated completion cookie handles COOP isolation. It grants no authority; the server checks the retried action. Cancel leaves the action unapplied. - R2: every credential insertion checks the resulting set in one SQL statement: at most 64 keys and 32 KiB of credential IDs plus UTF-8 key JSON. Failed insertions roll back setup, invitation and recovery grants. Larger legacy sets use discoverable authentication. Verification and removal still require the correct User, user verification and initiating live session. - Fixed the existing App Password queued-revocation race. A follower now joins the credential queue before checking mutation contention. All existing test assertions are unchanged. - Added a focused live-loopback auth probe runner and production-browser regression. The browser test covers real passkey and invitation writes, cancellation, touch, keyboard and 36 Mac-emulated review images. The OIDC-only read metadata and provider start/return are test fixtures; signed provider claims and session binding are tested in Rust. **Files** ``` apps/web/e2e/authfix-settings.mjs apps/web/src/lib/components/ConfirmSheet.svelte apps/web/src/routes/settings/[...path]/+page.svelte apps/web/src/routes/settings/account/AppPasswordsGroup.svelte apps/web/src/routes/settings/account/PasskeysGroup.svelte apps/web/src/routes/settings/account/PasskeysGroup.svelte.test.ts apps/web/src/routes/settings/account/ProfileGroup.svelte apps/web/src/routes/settings/account/RecoveryKeySheet.svelte apps/web/src/routes/settings/account/SessionsGroup.svelte apps/web/src/routes/settings/account/SignInMethodsGroup.svelte apps/web/src/routes/settings/admin/ConfigGroup.svelte apps/web/src/routes/settings/admin/InvitationsGroup.svelte apps/web/src/routes/settings/admin/InvitationsGroup.svelte.test.ts apps/web/src/routes/settings/admin/UsersGroup.svelte apps/web/src/routes/settings/api.svelte.test.ts apps/web/src/routes/settings/api.svelte.ts crates/calternal-auth/src/api.rs crates/calternal-auth/src/ceremony.rs crates/calternal-auth/src/passkey.rs crates/calternal-auth/src/store.rs ``` **Final gates — verbatim summary lines** `cargo fmt --check`: exit 0, no output. `cargo clippy -p calternal-auth --all-targets -- -D warnings` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 33m 03s ``` `RUST_TEST_THREADS=2 cargo test -p calternal-auth` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 32s test result: ok. 103 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 283.60s test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s ``` `cargo clippy -p calternal-server --all-targets -- -D warnings` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 10s ``` `cargo test -p calternal-server` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 30m 23s test result: ok. 161 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 16.53s ``` `bun run check` ``` svelte-check found 0 errors and 0 warnings ``` `bunx vitest run src/routes/settings/api.svelte.test.ts src/routes/settings/account/PasskeysGroup.svelte.test.ts src/routes/settings/admin/InvitationsGroup.svelte.test.ts src/lib/auth/passkeys.test.ts src/routes/settings/shared-components.guard.test.ts --maxWorkers=2` (from `apps/web`) ``` Test Files 5 passed (5) Tests 17 passed (17) Start at 20:31:14 Duration 17.31s (transform 59%, environment 18%, setup 10%, tests 7%, import 5%) ``` Focused live auth router over TCP: ``` test api::tests::local_auth_ceremony_probe_tcp ... PASS: 22 bounded local requests; invalid setup/JSON and oversized recovery refused; 16 admitted flows; capacity HTTP 429 test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 0.67s ``` Production browser: ``` PASS: Settings first-key and Admin invitation flows; six Mac width/theme pairs across changed screens; pointer, touch and keyboard; cancellation unapplied; COOP callbacks resume real writes ``` **Failures resolved and diagnostic checks — verbatim summaries** The first auth suite failed the queued App Password test. It also failed in isolation. The code fix passed its focused regression. The next suite failed while opening the signed OIDC test database, before its authentication case. That test passed in isolation. The final full auth suite passed with two test threads. No OIDC assertion or production timeout was changed. `r2-capacity` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 29m 33s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 103 filtered out; finished in 0.12s ``` `r2-legacy` ``` test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 103 filtered out; finished in 1.52s ``` `auth-clippy` ``` Finished `dev` profile [unoptimized + debuginfo] target(s) in 39m 50s ``` `auth-test` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 3m 19s test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 67.97s ``` `auth-revoke-focused` ``` test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.05s ``` `auth-revoke-fixed` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 8m 21s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 1.89s ``` `auth-test-final` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 7m 58s test result: FAILED. 102 passed; 1 failed; 3 ignored; 0 measured; 0 filtered out; finished in 71.98s ``` `auth-oidc-focused` ``` Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 30s test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 105 filtered out; finished in 54.02s ``` **UX gaps closed** - First-passkey and Admin forms can use a linked provider. Passkey-only text no longer describes these checks. - Cancel and Escape keep actions unapplied. Provider isolation does not lose the initiating form. A forged completion hint cannot grant authority. - The browser regression proves real writes before screenshot capture can spend the freshness window. Touch cancellation writes no key. - Error diagnostics redact temporary credential headers. **UX gaps left / known gaps** - #945 tracks the full-key-set error text. It uses the existing HTTP 429 wait message, although the User must remove a key to free capacity. This is a non-blocking text gap. - COOP can prevent the parent from closing the provider completion window. The original action resumes through its correlated hint. Actual-provider focus and Apple-client behavior still need the merge-round check. - No sign-in p95 claim is made. The supplied benchmark measures ceremony starts and explicitly has no auth-ceremony baseline. It does not measure Argon2 sign-in work. - Full web suites, full cross-User/Admin matrices, release builds and real Mac interoperability were not run, as required by the latest verification policy. **Decisions** - Choose the first configured provider that is linked to the User. No new provider picker is added. - Reuse the existing confirmation sheet and request helper. Open the provider window on explicit Continue so browsers retain User activation. - Use a five-minute, readable, digest-only completion hint for isolated windows. Keep the callback redirect contract and server authority checks. - Use the existing 64-key ceremony bound and one quarter of the 128 KiB reservation for inserted key-set bytes. Retain the existing 1024-byte credential-ID bound. Use discovery for larger legacy sets. **For the merge round** - `XUSER_MATRIX_ONLY=1 bash tests/adversarial/run.sh`: prove auth routes cannot cross User boundaries, including invalid and stale sessions. - `AUTHZ_MATRIX_ONLY=1 ADMIN_DENIAL_ONLY=1 bash tests/adversarial/run.sh`: prove denied Admin mutations stay unapplied. Run the full authz matrix once on the combined branch as well. - `cd apps/web && bun run test` and `bun run test:e2e:auth`: check the combined web changes and complete auth browser flows. - On the perf VM, with a prepared auth router and a shared build: `flock /root/perf.lock bash -c 'uptime; python3 bench/auth-ceremony.py --url "$AUTH_URL" --pid "$AUTH_PID" --output artifacts/auth-ceremony.json'`. This proves the bounded start profile only. A representative Argon2 sign-in profile and baseline are still needed to judge p95 noise. - Hold `~/.local/state/codex-jobs/calternal/macvm.lock` for real Mac checks: `flock -w 7200 ~/.local/state/codex-jobs/calternal/macvm.lock netbird ssh --no-browser calternal@10.69.69.21`. Check an actual configured provider in Safari: callback completion, parent focus, first-key creation, Admin mutation, cancellation and stale-session refusal. The owner must complete any credential prompts. **Cleanup** Fetched and merged `origin/dev` once (`c4faf184d`, merge `0bd0a05fd`). No push or deployment. Worktree is clean. Cargo cleanup: ``` Removed 15119 files, 6.6GiB total ``` Web build output and temporary test data are deleted. Screenshots remain in ignored `artifacts/authfix-settings/`; no review image is committed. **Final screenshots** Use this set for review. It matches the final browser regression. Earlier image links remain historical evidence. | Width / theme | First key | Account check | Invitations | Admin check | Users | Recovery intro | |---|---|---|---|---|---|---|---| | 390 / light | [Image](https://git.kayg.org/attachments/e1850ede-6804-4a4d-83f6-aaf7df72d75c) | [Image](https://git.kayg.org/attachments/374d2a0e-6f60-4a79-84a5-9dfbf1beb927) | [Image](https://git.kayg.org/attachments/1d743919-1c2e-43d7-8ca4-1a47bcb1fef5) | [Image](https://git.kayg.org/attachments/8824c13a-e454-474b-96f7-974d2b0acca5) | [Image](https://git.kayg.org/attachments/1ab439a8-99f8-4bcf-8426-73a695ae27e6) | [Image](https://git.kayg.org/attachments/ae06585e-bd23-4e57-89ee-b03c1b42eba9) | | 390 / dark | [Image](https://git.kayg.org/attachments/d36b5802-dbdb-4c65-aaf0-9986c741cb59) | [Image](https://git.kayg.org/attachments/f2449ec3-2172-4db1-9fd8-557d72f7034a) | [Image](https://git.kayg.org/attachments/3ba41d54-0558-4f5f-9f8f-91262dc18521) | [Image](https://git.kayg.org/attachments/2bd7afd8-073a-41b7-a505-02ac31dee59a) | [Image](https://git.kayg.org/attachments/cf643e3b-5139-47d1-a2a9-c7508f87fd6f) | [Image](https://git.kayg.org/attachments/e8912c40-dd1d-447c-97ab-c9368c24411e) | | 820 / light | [Image](https://git.kayg.org/attachments/2ece1bde-0b35-4348-8626-60671ed89b78) | [Image](https://git.kayg.org/attachments/0b10f470-c1b6-461f-9e88-0b2bef91435b) | [Image](https://git.kayg.org/attachments/b7de98a8-ace5-4712-a424-c27918aa90ea) | [Image](https://git.kayg.org/attachments/c935aacd-8146-4061-87a4-d268b427825d) | [Image](https://git.kayg.org/attachments/8370082b-6761-4abd-ad77-61b6399ade7f) | [Image](https://git.kayg.org/attachments/85d23b50-4c90-4ea5-bdf5-7c10587d1cfe) | | 820 / dark | [Image](https://git.kayg.org/attachments/167df5e6-f7f9-4d9e-a2a5-30e121780660) | [Image](https://git.kayg.org/attachments/33d34bce-5b07-46ad-a838-a8cbd50bb4bf) | [Image](https://git.kayg.org/attachments/1d9d77fa-f898-45db-87a7-0c8c6b1ae526) | [Image](https://git.kayg.org/attachments/4a1f6f2a-2ee1-447e-9e9d-987b622ac073) | [Image](https://git.kayg.org/attachments/f023fec2-a154-4e96-9f3a-9da1b6ede432) | [Image](https://git.kayg.org/attachments/91a05bb1-4282-4292-805b-7c3566dbf712) | | 1440 / light | [Image](https://git.kayg.org/attachments/b168adc6-53f3-4b9e-befe-79846b7d89e3) | [Image](https://git.kayg.org/attachments/0ab946c0-f8b0-4827-a31e-c7aa328c425d) | [Image](https://git.kayg.org/attachments/481992d7-c035-4018-b2e7-b8670f1209f4) | [Image](https://git.kayg.org/attachments/38b3c2d8-2bf4-42a1-a99a-cef2a036f7e1) | [Image](https://git.kayg.org/attachments/a6677b0c-b336-4a25-bab9-a3b231b65a82) | [Image](https://git.kayg.org/attachments/7406c67c-a15d-4931-98f3-7b0e38039f53) | | 1440 / dark | [Image](https://git.kayg.org/attachments/5af982fa-344e-48db-8ee8-317ec210ab6b) | [Image](https://git.kayg.org/attachments/2ccc7aca-17e8-4921-a683-cca3749c65dc) | [Image](https://git.kayg.org/attachments/45807601-d8bb-4895-b73e-6b5a16b8f399) | [Image](https://git.kayg.org/attachments/8c2681ac-88ec-440b-bc8a-e0e8b9cf02e6) | [Image](https://git.kayg.org/attachments/8e5f771b-19a1-4396-8399-b220d55a92df) | [Image](https://git.kayg.org/attachments/5019b1af-0b48-41dc-ac03-aed06be17eb7) |
Author
Owner

UI nit on job/authfix (not pushed): d86040522 fix(ui): stack a confirmation above the sheet it covers.

Cause: the "Confirm it’s you" step-up already used the shared ConfirmSheet (OverlaySurface: centred dialog on desktop, bottom sheet on phone). But every OverlaySurface used the same scrim/surface layer pair (200/201). So the confirmation's scrim sat below the Settings surface. The dialog floated over Settings content that was not dimmed and still took clicks (artifacts/authfix-settings/1440-light-admin-confirm.png).

Fix (shared primitive, so every ConfirmSheet over Settings gets it): OverlaySurface records its stack depth when it opens. It lifts its scrim and surface by depth × the new --layer-overlay-step token. A covered surface gets inert until the top surface closes. Escape still goes to the top surface only.

Test: OverlaySurface.svelte.test.ts "dims and disables the surface below a stacked confirmation (#734)".
Gates: bun run check: 0 errors, 0 warnings. Focused Vitest (OverlaySurface, InvitationsGroup, PasskeysGroup, settings api, themes, focusTrap, shared-components guard): 7 files, 103 tests passed. The review screenshots still need a new capture from a production build.

UI nit on `job/authfix` (not pushed): `d86040522` fix(ui): stack a confirmation above the sheet it covers. Cause: the "Confirm it’s you" step-up already used the shared ConfirmSheet (OverlaySurface: centred dialog on desktop, bottom sheet on phone). But every OverlaySurface used the same scrim/surface layer pair (200/201). So the confirmation's scrim sat below the Settings surface. The dialog floated over Settings content that was not dimmed and still took clicks (`artifacts/authfix-settings/1440-light-admin-confirm.png`). Fix (shared primitive, so every ConfirmSheet over Settings gets it): OverlaySurface records its stack depth when it opens. It lifts its scrim and surface by depth × the new `--layer-overlay-step` token. A covered surface gets `inert` until the top surface closes. Escape still goes to the top surface only. Test: `OverlaySurface.svelte.test.ts` "dims and disables the surface below a stacked confirmation (#734)". Gates: `bun run check`: 0 errors, 0 warnings. Focused Vitest (OverlaySurface, InvitationsGroup, PasskeysGroup, settings api, themes, focusTrap, shared-components guard): 7 files, 103 tests passed. The review screenshots still need a new capture from a production build.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#734
No description provided.