Skip to content

Spec: did:nostr to WebID Resolution #42

@melvincarvalho

Description

@melvincarvalho

Overview

This document specifies how JSS resolves did:nostr:<pubkey> identities to Solid WebIDs, enabling Nostr users to authenticate as their linked WebID.

Specification

Identity Linking

A bidirectional link between Nostr and Solid identities:

┌─────────────────┐         alsoKnownAs         ┌─────────────────┐
│   Nostr Profile │ ──────────────────────────► │   Solid WebID   │
│   (kind 0)      │                             │   (profile/card)│
│                 │ ◄────────────────────────── │                 │
└─────────────────┘         owl:sameAs          └─────────────────┘

DID Document Structure

The DID resolver at nostr.social/.well-known/did/nostr/<pubkey>.json returns:

{
  "@context": ["https://www.w3.org/ns/did/v1", "..."],
  "id": "did:nostr:<pubkey>",
  "alsoKnownAs": ["https://solid.social/alice/profile/card#me"],
  "verificationMethod": [...],
  "profile": { "name": "alice", ... }
}

WebID Profile Structure

The WebID at https://solid.social/alice/profile/card#me contains:

{
  "@context": { "owl": "http://www.w3.org/2002/07/owl#", ... },
  "@id": "#me",
  "@type": "foaf:Person",
  "foaf:name": "alice",
  "owl:sameAs": "did:nostr:<pubkey>"
}

Resolution Algorithm

1. Extract pubkey from NIP-98 event
2. Verify Schnorr signature
3. Fetch DID document:
   GET https://nostr.social/.well-known/did/nostr/<pubkey>.json
4. Extract WebID from:
   - alsoKnownAs[0] (preferred)
   - profile.webid
   - profile.sameAs
5. Fetch WebID profile:
   GET <webid> Accept: application/ld+json
6. Check for backlink:
   - owl:sameAs === did:nostr:<pubkey>
   - sameAs === did:nostr:<pubkey>
7. If bidirectional: return WebID
   Else: return did:nostr:<pubkey>

Authentication Flow

┌────────┐     NIP-98      ┌────────┐    Fetch DID    ┌─────────────┐
│ Client │ ──────────────► │  JSS   │ ──────────────► │ nostr.social│
└────────┘                 └────────┘                 └─────────────┘
                               │                            │
                               │ ◄──────────────────────────┘
                               │     DID Doc + alsoKnownAs
                               ▼
                          ┌────────┐
                          │ Fetch  │
                          │ WebID  │
                          └────────┘
                               │
                               ▼
                          ┌────────────────────┐
                          │ Verify owl:sameAs  │
                          │ matches did:nostr  │
                          └────────────────────┘
                               │
                          ┌────┴────┐
                          │ Match?  │
                          └────┬────┘
                         Yes   │   No
                          ▼    │    ▼
                    Return     │  Return
                    WebID      │  did:nostr

Implementation Recipe

Step 1: Create Nostr Keypair

import { generateSecretKey, getPublicKey } from 'nostr-tools';

const sk = generateSecretKey();
const pubkey = getPublicKey(sk);
console.log('Pubkey:', pubkey);
// e.g., 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d

Step 2: Create Solid Pod

curl -X POST https://solid.social/.pods \
  -H "Content-Type: application/json" \
  -d '{"name": "alice", "email": "alice@example.com", "password": "secret"}'

Response:

{
  "webId": "https://alice.solid.social/profile/card#me",
  "podUri": "https://alice.solid.social/"
}

Step 3: Add alsoKnownAs to Nostr Profile

Publish a kind 0 event with your WebID:

import { finalizeEvent, Relay } from 'nostr-tools';

const event = finalizeEvent({
  kind: 0,
  created_at: Math.floor(Date.now() / 1000),
  tags: [],
  content: JSON.stringify({
    name: 'alice',
    about: 'Solid + Nostr user',
    alsoKnownAs: ['https://alice.solid.social/profile/card#me']
  })
}, sk);

// Publish to relays
const relay = await Relay.connect('wss://relay.damus.io');
await relay.publish(event);

Step 4: Add owl:sameAs to WebID Profile

PATCH the WebID profile:

curl -X PATCH https://alice.solid.social/profile/card \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: text/n3" \
  -d '@prefix solid: <http://www.w3.org/ns/solid/terms#>.
      @prefix owl: <http://www.w3.org/2002/07/owl#>.
      _:patch a solid:InsertDeletePatch;
        solid:inserts { 
          <#me> owl:sameAs <did:nostr:YOUR_PUBKEY_HERE> .
        }.'

Step 5: Wait for DID Indexing

nostr.social indexes Nostr profiles periodically. Check:

curl https://nostr.social/.well-known/did/nostr/<pubkey>.json | jq .alsoKnownAs

Step 6: Authenticate with NIP-98

import { finalizeEvent } from 'nostr-tools';

// Create NIP-98 auth event
const authEvent = finalizeEvent({
  kind: 27235,
  created_at: Math.floor(Date.now() / 1000),
  tags: [
    ['u', 'https://alice.solid.social/private/data.json'],
    ['method', 'GET']
  ],
  content: ''
}, sk);

// Make authenticated request
const token = btoa(JSON.stringify(authEvent));
const res = await fetch('https://alice.solid.social/private/data.json', {
  headers: { 'Authorization': `Nostr ${token}` }
});

// JSS resolves did:nostr → WebID automatically
// You're now authenticated as https://alice.solid.social/profile/card#me

Caching

  • DID resolution results are cached for 5 minutes
  • Cache key: lowercase pubkey
  • Failed lookups are also cached to prevent hammering

Error Handling

Scenario Result
Invalid pubkey Return null
DID doc not found Return null, cache miss
No alsoKnownAs Return null
WebID fetch fails Return null
No owl:sameAs backlink Return null
Bidirectional link valid Return WebID

When resolution returns null, authentication falls back to did:nostr:<pubkey>.

Security Considerations

  1. Bidirectional proof required - Prevents claiming arbitrary WebIDs
  2. Signature verification first - DID resolution only after valid NIP-98
  3. HTTPS only - WebID must be HTTPS
  4. Cache prevents abuse - Rate limits external fetches

Related

  • NIP-98 - HTTP Auth
  • nostr-beacon - DID resolver
  • DID Core - W3C DID specification
  • src/auth/did-nostr.js - JSS implementation
  • src/auth/nostr.js - NIP-98 verification

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationnostrNostr relay, did:nostr auth, NIP-related

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions