Skip to content

auth: emit a device-registered event and a signed sign-out-everywhere link so an account owner learns of a new device #94

Description

@tonite31

Summary

@spfn/auth binds a login to a device key (per-device keypair, device-code approval from a signed-in device, listKeys / revokeKey / revokeAllKeys). What the account owner cannot do today is learn that a device was added. A stolen password used to register a new device is silent: nothing in the package fires when a key is registered, authLoginEvent carries provider but not the device, and there is no way to hand the owner a "sign every device out" action they can take from an email without first signing in on a device they may no longer trust.

Outcome

  1. A device registered event (authDeviceRegisteredEvent, auth.device.registered) emitted after a key is registered on any channel — password login on a new device, OAuth callback, device-code approval, passkey login — with { userId, keyId, deviceName?, platform?, algorithm, fingerprintPrefix, ip?, userAgent?, createdAtMillis, channel }. Nothing is emitted for key rotation of an existing device. The app subscribes and sends the notice; the package sends no mail.
  2. A signed sign-out-everywhere link. createRevokeAllLink(userId, { ttl }) returns a URL under /_auth/keys/revoke-all/<token>; opening it revokes every key of that user (including any device-code approval in flight, as revokeAllKeys already does), needs no session, is single-use, expires, and answers a plain confirmation page or a JSON result for an app-rendered page. The token is bound to the user's current key generation so it is dead once used or once revokeAllKeys ran by another path.
  3. listKeys grows ip? and userAgent? (captured at registration, never updated) so the device list an app renders can show where a device came from.

Narrow review dispatch contract

  • Required behavior: the event fires exactly once per new key on every registration channel; the link revokes all keys without a session, once, within its TTL; the list carries the two new optional fields.
  • Touched surfaces: key registration paths (register, login, oauth/finalize, device/*/approve, passkeys/login/verify), keys routes, Events docs.
  • Supported inputs: an existing user with ≥1 key; a token that is valid, expired, reused, or from a user whose keys were already revoked.
  • Trust boundary: the link token is a bearer capability — HMAC with the auth secret, ≥128-bit random id, constant-time compare, stored hash only; a reused or unknown token answers the same 404 as an expired one.
  • Explicit exclusions: sending email, a UI, MFA (separate issue), notifying on key rotation, geo-IP.
  • Stop condition: tests for each channel firing the event; tests for valid/expired/reused/revoked-elsewhere tokens; README section.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions