Skip to content

Commit f8904e9

Browse files
auth: did:nostr resolver prefers local index before external (JavaScriptSolidServer#423) (JavaScriptSolidServer#424)
When the requesting Nostr pubkey belongs to a local account, resolve to its WebID via the in-process well-known index instead of round- tripping through the configured external resolver. The local binding is already verified by construction (the index only admits VMs that are declared in `verificationMethod` AND referenced from `authentication` of a local profile), so we also skip the backlink verification the external path requires. Eliminates the most common SSO failure mode: user signs into their own pod, the auth path tries the external resolver (`nostr.social/.well-known/did/nostr/<pubkey>`) and the external host is unreachable (network down, SSL cert expired, etc.). With this change the external resolver is only consulted for cross-pod pubkeys that no local account claims. `verifyNostrAuth` (src/auth/nostr.js) already tries the local resolver first in its chain; duplicating that ordering inside `resolveDidNostrToWebId` itself is defense-in-depth — any caller from another code path (or any future caller that forgets the chain ordering) still gets the cheap, reliable answer for the dominant case. Dynamic import keeps the IdP module optional for builds that don't use it.
1 parent 1ee75c7 commit f8904e9

1 file changed

Lines changed: 38 additions & 8 deletions

File tree

src/auth/did-nostr.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
* DID:nostr Resolution
33
*
44
* Resolves did:nostr:<pubkey> to a Solid WebID by:
5-
* 1. Fetching DID document from nostr.social
6-
* 2. Extracting alsoKnownAs WebID
7-
* 3. Verifying bidirectional link (WebID links back to did:nostr)
5+
* 1. Trying the in-process local well-known index first (the pod is
6+
* its own authoritative resolver for its accounts — no HTTP)
7+
* 2. Falling back to the configured external resolver:
8+
* a. Fetching the DID document
9+
* b. Extracting alsoKnownAs WebID
10+
* c. Verifying bidirectional link (WebID links back to did:nostr)
811
*/
912

1013
import { validateExternalUrl } from '../utils/ssrf.js';
@@ -168,12 +171,21 @@ export async function fetchWithRedirectGuard(initialUrl, {
168171
}
169172

170173
/**
171-
* Resolve did:nostr pubkey to WebID via DID document.
174+
* Resolve did:nostr pubkey to WebID.
172175
*
173-
* Local users are resolved by `resolveDidNostrLocally` in the auth
174-
* caller (well-known-did-nostr.js exports an in-process function) —
175-
* this resolver is the cross-pod fallback that fetches an external
176-
* DID doc, so all fetches run through the SSRF guard.
176+
* Tries the in-process local well-known index first (the pod is its
177+
* own authoritative resolver for its accounts — no HTTP fetch, no
178+
* external dependency, no SSRF surface). Falls back to the configured
179+
* external resolver (`nostr.social` by default) only for cross-pod
180+
* pubkeys that no local account claims.
181+
*
182+
* This local-first ordering is also enforced in the `verifyNostrAuth`
183+
* resolver chain (src/auth/nostr.js). Duplicating it inside the
184+
* resolver itself is defense-in-depth: callers from other code paths
185+
* (and any future caller that forgets to consult the local index)
186+
* still get the cheap, reliable answer for the dominant SSO case
187+
* — user signing in to their own pod — even when the external
188+
* resolver is unavailable (network down, SSL cert expired, etc.).
177189
*
178190
* @param {string} pubkey - 64-char hex Nostr pubkey
179191
* @param {string} [resolverUrl] - DID resolver base URL (without the
@@ -193,6 +205,24 @@ export async function resolveDidNostrToWebId(pubkey, resolverUrl = DEFAULT_DID_R
193205
}
194206
pubkey = pubkey.toLowerCase();
195207

208+
// Local-first: consult the in-process well-known index before any
209+
// HTTP fetch. The index is built from local WebID profiles whose
210+
// `verificationMethod` declares this Nostr pubkey AND is referenced
211+
// from `authentication` (see src/idp/well-known-did-nostr.js) — so
212+
// the binding is already verified by construction and we skip the
213+
// backlink round-trip required for external answers.
214+
//
215+
// Dynamic import keeps the IdP module optional: pods built/run
216+
// without the IdP layer don't pull it in. Any import or lookup
217+
// error falls through to the external resolver.
218+
try {
219+
const { resolveDidNostrLocally } = await import('../idp/well-known-did-nostr.js');
220+
const localWebId = await resolveDidNostrLocally(pubkey);
221+
if (localWebId) return localWebId;
222+
} catch {
223+
// IdP module unavailable or local lookup threw — fall through.
224+
}
225+
196226
// Cache key includes the resolver URL because different resolvers
197227
// can legitimately disagree about the same pubkey (one might have
198228
// a DID doc, another not; alsoKnownAs values can differ across

0 commit comments

Comments
 (0)