QR device-handoff login ("scan to sign in") for PhoenixKit.
This is the PhoenixKit-side integration of the keyfob
library. Keyfob owns the rendezvous — request lifecycle, expiry,
single-use enforcement, the secret split, PubSub signaling. This module
wires it into PhoenixKit: the internal PubSub, the qr_login_enabled
setting, device-metadata extraction for the confirm screen, and activity
logging on approval.
The flow
- A signed-out browser opens
/users/qr-login(PhoenixKitWeb.Users.QrLogin) and a QR encoding the phone-confirm URL is shown. The LiveView waits. - The user's already-signed-in phone scans it, opens the confirm URL
(
PhoenixKitWeb.Users.QrLoginConfirm), reviews the requesting device, and taps Approve — which callsapprove/2with the phone user's uuid. - Approval mints a one-time login token delivered over PubSub only to
the waiting browser, which navigates to the completion controller
(
PhoenixKitWeb.Users.QrLoginComplete). - The controller calls
consume/1({:ok, user_uuid}, exactly once) and logs that user in with PhoenixKit's own session machinery.
Approval always happens on the trusted (phone) device; the browser gets
nothing until the phone approves. See keyfob's docs for the threat
model (QR-jacking, the secret split).
All keyfob calls are routed through PhoenixKit's internal PubSub
(:phoenix_kit_internal_pubsub) so the desktop LiveView and
the phone confirm LiveView rendezvous on the same bus. The default
Keyfob.Store.ETS (started in PhoenixKit.Supervisor) holds requests.
Summary
Functions
See Keyfob.approve/3. user_ref is the approving user's uuid.
See Keyfob.consume/2. Returns {:ok, user_uuid} exactly once.
See Keyfob.create_request/1. Merges the shared PubSub option.
See Keyfob.deny/2.
Extracts a device-description map from the requesting browser's connected socket, shown verbatim on the phone confirm screen so the human can recognise (or reject) the sign-in.
Whether QR device-handoff login is enabled (setting qr_login_enabled,
default false).
Shared keyfob options — routes every call through PhoenixKit's internal PubSub.
Formats a best-effort "City, Country" (or just "Country") string for
an IP, or nil when the lookup fails, is unavailable, or is still running
after 1500ms. Never raises — a geo backend hiccup must
not crash (or stall) the QR mint that shows the code.
Records a user.qr_login_approved activity for the approving user.
See Keyfob.peek/2.
The PubSub name every keyfob call in this integration uses.
Functions
See Keyfob.approve/3. user_ref is the approving user's uuid.
See Keyfob.consume/2. Returns {:ok, user_uuid} exactly once.
See Keyfob.create_request/1. Merges the shared PubSub option.
See Keyfob.deny/2.
@spec device_meta(Phoenix.LiveView.Socket.t()) :: map()
Extracts a device-description map from the requesting browser's connected socket, shown verbatim on the phone confirm screen so the human can recognise (or reject) the sign-in.
Keys: :browser, :os, :ip, :location — any of which may be absent
when the underlying connect-info (or geo lookup) is unavailable — plus
:requested_at, an absolute UTC timestamp of when the code was minted so
the approver can sanity-check "did I just do this?".
The geo lookup is a best-effort, timeout-bounded call on the requesting browser's IP (same machinery registration/login-alerts already use); it runs at QR-mint time so the location is baked into the request the phone later reads.
@spec enabled?() :: boolean()
Whether QR device-handoff login is enabled (setting qr_login_enabled,
default false).
@spec keyfob_opts() :: keyword()
Shared keyfob options — routes every call through PhoenixKit's internal PubSub.
Formats a best-effort "City, Country" (or just "Country") string for
an IP, or nil when the lookup fails, is unavailable, or is still running
after 1500ms. Never raises — a geo backend hiccup must
not crash (or stall) the QR mint that shows the code.
Records a user.qr_login_approved activity for the approving user.
Actor and target are the same user (they approved their own sign-in), so no notification is generated — this is an audit-trail entry only. Errors are swallowed: a logging failure must never block the sign-in.
See Keyfob.peek/2.
@spec pubsub() :: atom()
The PubSub name every keyfob call in this integration uses.
See Keyfob.subscribe/2.
See Keyfob.unsubscribe/2.