fix(auth): don't require a Nostr pubkey to be considered logged in #180

Merged
padreug merged 1 commit from fix/auth-guard-no-pubkey-requirement into dev 2026-10-08 18:46:04 +00:00
Owner

Symptom

Logging into the wallet standalone redirects straight back to /login, forever. Same for accounting. Reproduced on atio.

Cause

src/lib/router-helpers.ts:

function isFullyAuthed(auth: AuthLike): boolean {
  return auth.isAuthenticated.value && !!auth.currentUser?.value?.pubkey
}

For an account with no Nostr pubkey this is a closed loop: login succeeds, lnbits issues a token, isAuthenticated flips true — but pubkey is empty, so isFullyAuthed stays false and installStrictAuthGuard bounces every route back to /login.

It bites wallet, chat and accounting (the three strict-guard apps). On atio, 9 of 58 accounts have a Nostr identity; the other 49 are Lightning-only and locked out.

The pubkey check was never about needing an identity. It was added for #36 as a proxy for "getCurrentUser() actually came back", so a stale token in localStorage couldn't count as a session. Pubkey happened to be a reliable marker — until a population without one showed up.

Fix

Key the check on id instead. Every account has one, so the #36 protection is preserved exactly (a bare token still doesn't satisfy it) and the incidental identity requirement goes away.

Then put the genuine requirement where it actually belongs — installStrictAuthGuard takes an optional { requirePubkey }, and only chat sets it:

installStrictAuthGuard(router, { requirePubkey: true })   // chat only

Chat is Nostr end-to-end and can't function without a key. Wallet and accounting are payment/ledger surfaces, and per ADR-0001 in aiolabs/lnbits — LNbits is a liquidity service, not an identity provider — core payment flows must not be gated on having an identity. Being unable to see your Lightning balance without a Nostr key is that principle inverted.

Verification

  • The wallet genuinely never reads the user's pubkey. Its only nostr-tools import is nip19.encodeBytes('lnurl', …) in WalletPage.vue, used for bech32-encoding a pay link into the receive QR — a codec, nothing to do with identity.
  • vue-tsc -b passes.
  • All three call sites typecheck; the new parameter is optional, so wallet and accounting are untouched.
  • installLenientAuthGuard shares isFullyAuthed, so the public standalones' meta.requiresAuth routes get the same fix for free.

Known gap, deliberately not fixed here

A pubkey-less user opening chat still loops at /login instead of seeing an "identity required" screen. That loop is now correct behaviour rather than a bug, but it's still a poor way to express it and deserves a proper message. Happy to split that into its own issue.

Context

Surfaced while moving atio onto the lnbits dev channel (aiolabs/lnbits#78). atio is the first instance with a large Lightning-only population, which is why this went unnoticed — every other instance's accounts happen to have pubkeys.

## Symptom Logging into the wallet standalone redirects straight back to `/login`, forever. Same for accounting. Reproduced on atio. ## Cause `src/lib/router-helpers.ts`: ```ts function isFullyAuthed(auth: AuthLike): boolean { return auth.isAuthenticated.value && !!auth.currentUser?.value?.pubkey } ``` For an account with no Nostr pubkey this is a closed loop: login succeeds, lnbits issues a token, `isAuthenticated` flips true — but `pubkey` is empty, so `isFullyAuthed` stays false and `installStrictAuthGuard` bounces every route back to `/login`. It bites **wallet, chat and accounting** (the three strict-guard apps). On atio, 9 of 58 accounts have a Nostr identity; the other 49 are Lightning-only and locked out. The pubkey check was never about needing an identity. It was added for #36 as a proxy for *"`getCurrentUser()` actually came back"*, so a stale token in localStorage couldn't count as a session. Pubkey happened to be a reliable marker — until a population without one showed up. ## Fix Key the check on `id` instead. Every account has one, so the #36 protection is preserved exactly (a bare token still doesn't satisfy it) and the incidental identity requirement goes away. Then put the genuine requirement where it actually belongs — `installStrictAuthGuard` takes an optional `{ requirePubkey }`, and only chat sets it: ```ts installStrictAuthGuard(router, { requirePubkey: true }) // chat only ``` Chat is Nostr end-to-end and can't function without a key. Wallet and accounting are payment/ledger surfaces, and per **ADR-0001** in `aiolabs/lnbits` — *LNbits is a liquidity service, not an identity provider* — core payment flows must not be gated on having an identity. Being unable to see your Lightning balance without a Nostr key is that principle inverted. ## Verification - **The wallet genuinely never reads the user's pubkey.** Its only `nostr-tools` import is `nip19.encodeBytes('lnurl', …)` in `WalletPage.vue`, used for bech32-encoding a pay link into the receive QR — a codec, nothing to do with identity. - `vue-tsc -b` passes. - All three call sites typecheck; the new parameter is optional, so `wallet` and `accounting` are untouched. - `installLenientAuthGuard` shares `isFullyAuthed`, so the public standalones' `meta.requiresAuth` routes get the same fix for free. ## Known gap, deliberately not fixed here A pubkey-less user opening **chat** still loops at `/login` instead of seeing an "identity required" screen. That loop is now *correct* behaviour rather than a bug, but it's still a poor way to express it and deserves a proper message. Happy to split that into its own issue. ## Context Surfaced while moving atio onto the lnbits dev channel (`aiolabs/lnbits#78`). atio is the first instance with a large Lightning-only population, which is why this went unnoticed — every other instance's accounts happen to have pubkeys.
isFullyAuthed() keyed the "server confirmed this session" check on
currentUser.pubkey. Every account has an id; a pubkey is optional, so
Lightning-only accounts were locked out of every strict-guard app:
login succeeded, isAuthenticated flipped true, pubkey stayed empty,
isFullyAuthed returned false, and the guard redirected back to /login --
a loop with no way out. Observed on atio, where 9 of 58 accounts have a
Nostr identity and the other 49 do not.

The pubkey was only ever a proxy for "getCurrentUser() came back", added
for issue #36 to stop a bare localStorage token counting as a session.
Keying on id preserves that protection exactly and drops the incidental
identity requirement.

Also gates the genuine requirement where it belongs: chat is Nostr
end-to-end and now opts in via installStrictAuthGuard(router,
{ requirePubkey: true }). Wallet and accounting are payment/ledger
surfaces -- per ADR-0001 (aiolabs/lnbits) LNbits is a liquidity service,
not an identity provider, so core payment flows must not be gated on
having an identity. Verified the wallet module never reads the user's
pubkey; its only nostr-tools import is nip19.encodeBytes for LNURL
bech32 in the receive QR, unrelated to identity.

Known gap, not addressed here: a pubkey-less user opening chat still
loops at /login rather than seeing an "identity required" screen. The
loop is now correct behaviour instead of a bug, but it deserves a real
message.

vue-tsc -b passes; all three call sites typecheck (the new parameter is
optional, so wallet and accounting are unchanged).
padreug deleted branch fix/auth-guard-no-pubkey-requirement 2026-10-08 18:46:04 +00:00
Sign in to join this conversation.
No description provided.