Skip to content

Implement Automatic NIP-98 HTTP Authentication #1

@melvincarvalho

Description

@melvincarvalho

Overview

Currently, Podkey provides NIP-07 window.nostr API for websites to manually create and sign NIP-98 auth events, but it does not automatically intercept HTTP requests to add NIP-98 Authorization headers. This issue tracks the implementation of automatic NIP-98 authentication for Solid servers and other Nostr-authenticated HTTP endpoints.

Current State

  • ✅ Extension has webRequest permission in manifest.json
  • ✅ Extension can sign NIP-98 events (kind 27235) via window.nostr.signEvent()
  • ✅ Auto-sign functionality exists for trusted origins
  • No automatic HTTP request interception
  • No automatic 401 response detection and retry
  • No automatic Authorization header injection

NIP-98 Specification Reference

Specification: https://github.com/nostr-protocol/nips/blob/master/98.md

Event Structure

{
  "kind": 27235,
  "content": "",
  "tags": [
    ["u", "https://example.com/api/resource?query=value"],
    ["method", "GET"]
  ],
  "created_at": 1682327852
}

Authorization Header Format

Authorization: Nostr <base64-encoded-signed-event>

For Requests with Body (POST, PUT, etc.)

Include a payload tag with SHA-256 hash of the request body.

Implementation Requirements

1. HTTP Request Interception

Location: src/background.js

  • Use chrome.webRequest.onBeforeSendHeaders to intercept outgoing requests
  • Filter requests to origins that are trusted or are known Solid servers
  • Only intercept requests to origins that have been trusted or are known Solid servers
  • Don't intercept requests that already have an Authorization header (unless it's a retry)
  • Cache signed auth events per request URL+method to avoid re-signing identical requests

2. 401 Response Detection and Retry

Location: src/background.js

  • Use chrome.webRequest.onHeadersReceived to detect 401 responses
  • When 401 is detected:
    1. Check if origin is trusted
    2. Create NIP-98 auth event for the failed request
    3. Sign the event using existing signEvent() function
    4. Retry the original request with Authorization: Nostr <base64-event> header

Retry Logic:

  • Only retry on 401 Unauthorized responses
  • Consider 403 Forbidden if it's auth-related (optional, may need user preference)
  • Do NOT retry on other 4xx errors (404, 400, etc.) - these are not auth issues
  • Limit retry attempts (max 1 retry per request to avoid loops)
  • Track retry state to prevent infinite loops

3. NIP-98 Event Creation

Location: src/background.js (new function)

async function createNip98AuthEvent(requestDetails) {
  const event = {
    kind: 27235,
    content: '',
    created_at: Math.floor(Date.now() / 1000),
    tags: [
      ['u', requestDetails.url],
      ['method', requestDetails.method]
    ]
  };
  
  // If request has body, add payload tag
  if (requestDetails.requestBody) {
    const bodyHash = await hashRequestBody(requestDetails.requestBody);
    event.tags.push(['payload', bodyHash]);
  }
  
  return event;
}

4. Request Body Hashing

  • For requests with body (POST, PUT, PATCH), compute SHA-256 hash
  • Use existing @noble/hashes library
  • Handle different body formats: FormData, ArrayBuffer, Blob, string

5. Base64 Encoding

  • Encode signed event JSON to base64
  • Format: Authorization: Nostr <base64-string>

6. User Preferences

  • Add setting: "Auto-authenticate HTTP requests" (separate from event auto-sign)
  • Add setting: "Retry on 403 Forbidden" (optional, default false)
  • Respect existing getAutoSign() preference

Code Structure

WebRequest Listeners

// Intercept outgoing requests
chrome.webRequest.onBeforeSendHeaders.addListener(
  interceptRequest,
  {
    urls: ['<all_urls>'],
    types: ['xmlhttprequest', 'main_frame', 'sub_frame']
  },
  ['requestHeaders', 'blocking']
);

// Detect 401 responses
chrome.webRequest.onHeadersReceived.addListener(
  handle401Response,
  {
    urls: ['<all_urls>'],
    types: ['xmlhttprequest', 'main_frame', 'sub_frame']
  },
  ['responseHeaders']
);

Edge Cases

  1. Request Body Handling: Different formats (FormData, Blob, ArrayBuffer, string)
  2. URL Normalization: Ensure u tag matches exactly what server expects
  3. Timing: created_at should be recent (within 60 seconds typically)
  4. Caching: Cache signed events per (URL, method, bodyHash) tuple
  5. Security: Only auto-auth for trusted origins
  6. Performance: Minimize blocking operations
  7. Error Handling: Handle signing failures gracefully

Acceptance Criteria

  • Extension automatically adds NIP-98 Authorization header to requests from trusted origins
  • Extension detects 401 responses and retries with NIP-98 auth
  • NIP-98 events are correctly formatted per specification
  • Request bodies are hashed and included in payload tag when present
  • Only 401 (and optionally 403) responses trigger retry
  • User preferences (auto-sign, trusted origins) are respected
  • No infinite retry loops
  • Works with GET, POST, PUT, DELETE, PATCH methods
  • Handles various request body formats correctly
  • Unit tests pass
  • Integration tests pass

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions