Files
bitlogin/docs/login-methods.md
thePR0M3TH3ANandClaude Fable 5 8079d90cf8 Replace OAuth on-ramps with passkey sign-in: zero registration, zero servers
Owner decision: static only, and no per-site provider registration --
which every OAuth path requires (and GitHub/Facebook additionally require
a server). Both on-ramp implementations (service handshake, serverless
Google Drive) are removed the day they shipped; centralized-onramps.md
stays on the record as the analysis of why, superseded by
passkey-login.md.

The passkey rail: a WebAuthn PRF-capable passkey (synced for most users
by Google Password Manager or iCloud Keychain) deterministically derives
the login name and password of a standard capsule account -- passkey.ts
holds the derivation behind a FROZEN v1 known-answer contract with the
same standing rule as knownAnswer.test.ts. The ordinary password flows
do everything else; nothing is stored outside the user's authenticator,
and the rail needs no network of its own.

Flow is idempotent by construction: sign-in against a passkey with no
account routes into setup with the derived credential
(AccountNotFoundError); setup against an existing account signs in
(AccountAlreadyExistsError). Fresh identity or §SF10 nsec import at
setup; Tier B2 phrase card retained ('your account stays yours even if
you lose your passkey'); rotation is the graduation step and says so.

Pins updated: passkey row with honest custody label inside the menu,
OAuth actions provably absent, element still network-free. 11 passkey
tests incl. the frozen vectors; menu and screens verified in Chrome.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-04 12:20:26 -04:00

251 lines
13 KiB
Markdown

# BitLogin as the Single Login Surface — NIP-07 and NIP-46 Methods
**Document version:** 0.3
**Status:** §LM4 (NIP-07) and §LM5 (NIP-46, memory-only phase) are
**implemented** (2026-08-04): `signers/` in `@bitlogin/widget`, the NIP-46
client in `@bitlogin/core`'s `nostr/nip46.ts` with the ephemeral key held in
the crypto worker, both `bunker://` paste and the nostrconnect QR (rendered
as dot-style inline SVG), thin sessions per §LM7, password-first per §LM9.1.
NIP-46 *persistence* (§LM5.3 phase two) remains gated on the external vault
review. §LM6 records a settled rejection (nsec paste as a login method),
kept on the record.
**Depends on:** `docs/spec.md` (§ references point there unless marked §LM or
§SF), `docs/second-factor.md` §SF10 (imported identities, implemented)
---
# LM1. Purpose and scope
Today `<bitlogin-auth>` offers exactly one way in: a BitLogin login name and
password. A site that wants to serve existing Nostr users — extension holders,
bunker users — has to build its own login chooser and treat BitLogin as one
button in that menu.
This proposal inverts that. The widget becomes the login menu: one embed, one
host-facing surface, and the user picks their method behind it:
1. **BitLogin login name + password** — the existing derived-account flow,
unchanged, and still the default presented to new users.
2. **NIP-07** — delegate to a signer extension already installed in the
browser (§LM4).
3. **NIP-46** — connect to a remote signer ("bunker") over relays (§LM5).
§26.4 already names NIP-07 and NIP-46 as anticipated future signer support;
this document is the design for that line.
Out of scope, permanently: accepting a pasted `nsec` as a login method
(§LM6). Users holding a raw key are routed to the implemented §SF10 import
flow instead, which wraps the key into a full BitLogin account once rather
than handling it at every sign-in.
# LM2. Why aggregate rather than be aggregated
The bottleneck for BitLogin is developer adoption, and a developer evaluating
a login widget hears one objection immediately: *my existing users have
extensions and bunkers; a password-only widget doesn't cover them.* Supporting
all three methods behind one embed removes that objection, and every embed
then distributes BitLogin's native method as the easy path for everyone who
does not already have Nostr keys.
Prior art validates the shape: `nostr-login` (nostrband) is an embeddable
widget aggregating extension, NIP-46, and other methods, and it occupies the
"one login surface" niche today. BitLogin's differentiator is not the
aggregator layer — that part is commodity — but the derived-account custody
underneath it. The aggregator is worth building because it is the wedge that
gets the custody in front of users, not because it is novel.
The positioning consequence is worth stating plainly: with this change, a site
never needs a login UI other than BitLogin, and BitLogin's security story —
*no method ever puts a root key on the page* — holds across every method it
offers (§LM6).
A fourth method — passkey sign-in, where a WebAuthn PRF-capable passkey
(synced by the user's Google/Apple account) deterministically derives the
credential of a standard capsule account with zero site setup and zero
servers — is designed in [`passkey-login.md`](./passkey-login.md). The OAuth
on-ramps considered before it are superseded and kept on the record in
[`centralized-onramps.md`](./centralized-onramps.md).
# LM3. Architecture: one facade, pluggable signers
The widget already exposes a NIP-07-shaped provider — installed as
`window.nostr` (element.ts installs and releases it, taking over from an
extension where configurable) and mirrored as an element-scoped API. Hosts
program against that facade, not against the derivation machinery. That
boundary is exactly where the abstraction goes.
```text
host page ── window.nostr / <bitlogin-auth> API (unchanged surface)
│
provider facade
│
active Signer
┌──────┼──────────────┐
DerivedSigner Nip07Signer Nip46Signer
(existing (delegates (bunker client
worker path) to the over relays)
extension's
provider)
```
A `Signer` implements the calls the facade already promises — `getPublicKey`,
`signEvent`, NIP-44 and NIP-04 encrypt/decrypt — plus metadata:
- `method`: `"bitlogin" | "nip07" | "nip46"`, surfaced to the host in the
sign-in event so applications can adapt (e.g. hide password-rotation UI for
non-BitLogin sessions).
- `capabilities`: which optional calls this signer actually supports.
Extensions vary — some lack `nip44` — and the facade must report honestly
rather than throw surprises (§LM4).
Rules that hold for every signer:
- **No silent hangs.** Every facade call has a timeout and a typed failure.
This restates the Tier 1 rule from the 2026-07-30 audit for a world where a
signature may wait on a phone that is asleep (§LM5).
- **The facade is the only surface.** No signer leaks its backend (extension
provider object, bunker connection, worker handle) to the host page.
- **One active signer per element.** Method choice happens at sign-in;
switching methods is a sign-out and sign-in, composing with (not
complicating) the account-switching design.
# LM4. NIP-07: delegate to an installed extension
The cheapest method: no key custody, no new stored secrets, no relay traffic.
The widget detects an injected `window.nostr` that is not its own, offers
"Sign in with extension," and the `Nip07Signer` passes facade calls through
to the extension's provider.
Design points:
1. **Arbitration inverts, not disappears.** The current element takes over
`window.nostr` at sign-in and warns when an extension's property is
non-configurable. With a NIP-07 session the widget must *not* fight the
extension — the extension's provider is the backend. The existing
install/release logic stays for BitLogin-method sessions; NIP-07 sessions
leave `window.nostr` alone (or install a passthrough only where the
extension injected late). The detection must also snapshot the extension's
provider *before* the widget's own install, so the two never confuse each
other.
2. **Capability probing.** `nip44` presence is checked on the extension
object and reported in `capabilities`; absent calls reject with a typed
"unsupported by this signer" error rather than a TypeError from an
undefined property.
3. **User-visible identity.** After `getPublicKey`, the widget shows the
`npub` it got, so the user can notice a wrong-profile extension state
before the host application acts on it.
4. **No session material of its own.** A NIP-07 session persists nothing —
on reload the extension is simply asked again. This makes NIP-07 the
right first shipping increment (§LM8).
# LM5. NIP-46: connect to a remote signer
The user's key lives in a bunker (nsecBunker, Amber, or similar); the widget
holds only a client keypair and a connection secret, and requests signatures
over relays. This is the method most aligned with BitLogin's security story —
the widget custodies a *revocable credential*, never the key. If a
compromised host exfiltrates the connection, the user revokes it at the
signer; contrast a stolen `nsec`, which is unrotatable (§LM6).
Design points:
1. **Connect flow.** Accept a `bunker://` URI or nostrconnect handshake;
surface the signer's `auth_url` approval ceremony inside the widget UI
(open in a new tab, show pending state, resume on approval). The connect
ceremony is widget UI work of the same kind as the NWC REVEAL flow that
shipped 2026-07-30.
2. **Latency is a feature of the method, not a bug to hide.** Every
`signEvent` is a relay round trip to a device that may be offline. The
facade shows a pending state, applies a timeout, and fails with a typed,
retryable error. A host that got instant signatures from the derived
path must not discover NIP-46 latency as a hang.
3. **Persistence of the connection secret — deliberately phased.**
- *Phase one: memory-only.* The connection lives for the session; a
reload means reconnecting. Nothing new is stored anywhere, so nothing
new needs review.
- *Phase two: a Connection Vault record.* A NIP-46 connection is shaped
like the NWC credentials the vault already models — relay URLs plus a
secret, revocable at the far end, scoped in authority. Persisting it as
a vault record profile falls squarely under the standing gate:
**external security review before the vault holds anything beyond a
budget-capped NWC connection.** Phase two therefore waits on that
review; phase one does not.
4. **Client keypair hygiene.** The ephemeral client key for the NIP-46
conversation is generated by the CSPRNG chokepoint like every other key
in the codebase, and lives in the worker, not the DOM thread.
# LM6. Rejected: nsec paste as a login method
Considered and rejected, on the record.
A paste box for `nsec` on an embedded, same-origin widget means the root key
transits every host page that embeds BitLogin, at every sign-in, forever. An
XSS on *any* embedding site exfiltrates a key that Nostr provides no way to
rotate. §SF10 already states the principle: *the paste is the risk* — entering
an `nsec` into a web page is the most dangerous single action in Nostr use.
A login method institutionalizes that action as a habit; it also breaks the
claim §LM2 leans on, that no BitLogin method ever puts a root key on the page.
The sanctioned path for a user holding a raw key is the **implemented §SF10
import flow**: paste once, on a build you trust, and the key is wrapped into
a full BitLogin account — capsules built around the imported key, a fresh
recovery phrase, password sign-in from then on. The login surface should
route to it: an "I have an nsec" affordance that opens the import flow, so
the user's need is met without the recurring paste.
The distinction in one line: **import custodies the key once, under the
account's full protections; a login method would handle it on every sign-in,
under none of them.**
# LM7. Sessions are thin for non-BitLogin methods
Everything in the portable account layer — Connection Vault, portable app
state, entitlements, second factor — is built on capsule custody. A NIP-07 or
NIP-46 session has no capsule, so in this design those sessions are **signer
sessions only**: the host gets `window.nostr`, the sign-in event with
`method` and `capabilities`, and nothing else. No vault, no portable state,
no second factor.
This is deliberate:
- It keeps the aggregator commodity-cheap — the expensive, review-gated
machinery stays exclusive to derived accounts.
- It creates the honest upsell: "create a BitLogin password to get your
settings and connections on every device." The widget may offer this to a
signed-in NIP-07/NIP-46 user; note that upgrading an extension or bunker
identity to a *full* account requires the private key BitLogin never sees
for those methods, so the upsell mints a derived account (new identity) or,
for a user who separately holds their key, points at §SF10 import.
Extending capsule-backed features to non-derived identities is possible in
principle but is a protocol-surface expansion this document explicitly does
not propose.
# LM8. Phasing
1. **NIP-07** — smallest increment: detection, delegation, capability
probing, arbitration inversion. No new secrets, no relay traffic, no
review gate touched. **Shipped 2026-08-04.**
2. **NIP-46, memory-only** — connect ceremony (bunker:// paste and a
nostrconnect QR), pending/timeout UX, typed errors. Still nothing
persisted. **Shipped 2026-08-04.** Transport is NIP-44 only, per the
current NIP-46 spec; legacy NIP-04-only bunkers are not supported.
3. **NIP-46 persistence as a vault profile** — after the external security
review the vault is already gated on.
Each step ships independently; the login UI grows one option at a time.
# LM9. Open questions
1. **Resolved 2026-08-04 (owner decision):** username/password stays the
primary presentation; alternative methods appear as secondary affordances
below it, never promoted above the password path even when an extension is
detected. Pinned by `securityPolicy.test.ts`.
2. Should a NIP-07 session survive reload by silently re-asking the
extension, or require an explicit click each visit (extension prompt
fatigue vs. surprise sessions)?
3. Does the sign-in event schema need versioning now that `method` and
`capabilities` are added to it?
4. Whether the widget should expose "which methods are enabled" as element
attributes so a host can, e.g., disable NIP-46 (`methods="bitlogin,nip07"`).