You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
verifyBearerAuth refuses a request before any handler runs and records nothing in the
audit trail. It writes an application log line and returns
(src/middleware/verifyBearerAuth.ts:23, :31, :46), and there is no AuthEventService call in the file. So an operator can see failed OTP codes, failed
assertions and locked accounts in auth_events, but not a caller repeatedly presenting
the wrong kind of token at a protected route.
This is long-standing and applies to every gated route. It is worth a ticket now because
moving passkey enrollment behind the access session made one specific case invisible that
previously was not.
What changed to surface it
Before, /webauthn/register/* accepted an ephemeral token and declared decoy responders,
which logged webauthn:register_start and webauthn:register_finish on every decoy hit.
Enrollment now takes auth: 'access', so an ephemeral token is refused in middleware and
the decoys are gone. The refusal is the right answer. The gap is that the attempt it
refuses, which is exactly the account-takeover probe the gate was added to stop, now
leaves no durable trace.
Raised as a sub-threshold observation in the security review of that change: consistent
with how every other access-gated route behaves, so not a regression, but detection of
enrollment-gate probing is weaker than it was.
Suggested shape
Record the refusal centrally in verifyBearerAuth rather than adding it per route, since
the gap is general:
an event type for a rejected bearer, carrying the expected token type, the route, and
the reason (missing, malformed, wrong typ, unknown kid, expired),
userId: null, since a refused token has not established a subject. A token that
verified but was the wrong type does carry a sub, and whether to record it is a
judgement call worth making deliberately rather than by accident.
Two things to get right:
Volume. This fires on every expired access token, which is ordinary traffic, not an
attack. It needs either a narrower trigger than "any 401" or the retention story in Audit events have no retention policy and no bulk export #173
to land first, or it will bury the signal it is meant to surface.
Not an enumeration oracle. The event is written server side and never reaches the
caller, so it does not change any response, but the decoy rules in docs/security-posture.md are worth re-reading before adding writes to a pre-auth path.
Summary
verifyBearerAuthrefuses a request before any handler runs and records nothing in theaudit trail. It writes an application log line and returns
(
src/middleware/verifyBearerAuth.ts:23,:31,:46), and there is noAuthEventServicecall in the file. So an operator can see failed OTP codes, failedassertions and locked accounts in
auth_events, but not a caller repeatedly presentingthe wrong kind of token at a protected route.
This is long-standing and applies to every gated route. It is worth a ticket now because
moving passkey enrollment behind the access session made one specific case invisible that
previously was not.
What changed to surface it
Before,
/webauthn/register/*accepted an ephemeral token and declared decoy responders,which logged
webauthn:register_startandwebauthn:register_finishon every decoy hit.Enrollment now takes
auth: 'access', so an ephemeral token is refused in middleware andthe decoys are gone. The refusal is the right answer. The gap is that the attempt it
refuses, which is exactly the account-takeover probe the gate was added to stop, now
leaves no durable trace.
Raised as a sub-threshold observation in the security review of that change: consistent
with how every other access-gated route behaves, so not a regression, but detection of
enrollment-gate probing is weaker than it was.
Suggested shape
Record the refusal centrally in
verifyBearerAuthrather than adding it per route, sincethe gap is general:
the reason (missing, malformed, wrong
typ, unknownkid, expired),userId: null, since a refused token has not established a subject. A token thatverified but was the wrong type does carry a
sub, and whether to record it is ajudgement call worth making deliberately rather than by accident.
Two things to get right:
attack. It needs either a narrower trigger than "any 401" or the retention story in Audit events have no retention policy and no bulk export #173
to land first, or it will bury the signal it is meant to surface.
caller, so it does not change any response, but the decoy rules in
docs/security-posture.mdare worth re-reading before adding writes to a pre-auth path.Related