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
- Bidirectional proof required - Prevents claiming arbitrary WebIDs
- Signature verification first - DID resolution only after valid NIP-98
- HTTPS only - WebID must be HTTPS
- 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
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:
DID Document Structure
The DID resolver at
nostr.social/.well-known/did/nostr/<pubkey>.jsonreturns:{ "@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#mecontains:{ "@context": { "owl": "http://www.w3.org/2002/07/owl#", ... }, "@id": "#me", "@type": "foaf:Person", "foaf:name": "alice", "owl:sameAs": "did:nostr:<pubkey>" }Resolution Algorithm
Authentication Flow
Implementation Recipe
Step 1: Create Nostr Keypair
Step 2: Create Solid Pod
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:
Step 4: Add owl:sameAs to WebID Profile
PATCH the WebID profile:
Step 5: Wait for DID Indexing
nostr.social indexes Nostr profiles periodically. Check:
Step 6: Authenticate with NIP-98
Caching
Error Handling
When resolution returns null, authentication falls back to
did:nostr:<pubkey>.Security Considerations
Related
src/auth/did-nostr.js- JSS implementationsrc/auth/nostr.js- NIP-98 verification