Skip to content

stoic-one/ns-auth-sdk

Repository files navigation

Stoic Identity

The simplest way of doing Auth with seamless and decentralized Key-Management for SSO, Authentication, Membership, and Profile-Management

Who is it for?

Being trusted by Financial Institutions for Client Onboarding and Digital Communities for Membership Management. Examples include NSAuth and OneBoard. The SDK enables client-side managing of private-keys with WebAuthn passkeys (FIDO2 credentials). By leveraging passkeys, users avoid traditional private‑key backups and password hassles, relying instead on biometric or device‑based authentication. The keys are compatible with common blockchains like Bitcoin and Ethereum and data is stored as events on public relays and can be encrypted.

The Open Alternative for Auth

Open‑source, client‑side, decentralized single‑sign‑on (SSO) like NSAuth is superior because it puts the user’s identity and cryptographic keys directly in the hands of the individual, eliminating reliance on any central authority that could become a single point of failure, a privacy sinkhole, or a bottleneck for policy updates. By storing a self‑sovereign credential on the device’s secure enclave and validating access against a signed, versioned member list, every interaction—from unlocking a gym door to logging into an online course is verified instantly without ever transmitting personal identifiers. This architecture enables real‑time privilege changes (a badge upgrade or a revocation propagates the moment the list is updated), removes passwords and phishing risk through biometric or hardware‑key authentication, and works uniformly for anyone, including stateless persons or diaspora communities, because trust is derived from cryptographic proofs rather than government‑issued IDs. Moreover, being open source lets developers audit the code, contribute improvements, and ensure transparency, while the decentralized design guarantees that no single entity can unilaterally alter membership rules, providing stronger governance, auditability, and privacy than traditional, server‑centric SSO solutions.

Technology

Choose your Backend

NSAuth is designed as a frontend SSO where data can be synced in a trust minimized way. The basics are there for extensions to interoperate with public or private DLT networks or even regular webservers.

Two Approaches

PRF Direct Method

Derive the private key directly from the PRF value produced by a passkey.

Password Fallback Method

If PRF is not supported (e.g., on older browsers or devices without WebAuthn), the SDK automatically falls back to a password-protected key. The user's password encrypts the private key using AES-256-GCM, and the encrypted bundle is stored locally. The password is never stored - it's only used to derive the encryption key on-demand.

Encryption Method

Encrypt an existing private key with a key derived from the passkey’s PRF output. WebAuthn PRF Extension The PRF (Pseudo‑Random Function) extension, part of WebAuthn Level 3, yields deterministic 32‑byte high‑entropy values from an authenticator’s internal private key and a supplied salt. The same credential ID and salt always generate the same PRF output, which never leaves the device except during authentication.

Using PRF Values as Private Keys

A 32‑byte PRF output can serve as a private key if it falls within the secp256k1 range (1 ≤ value < n). The chance of falling outside this range is astronomically low (~2⁻²²⁴), so explicit range checks are generally unnecessary.

Restoration Steps

Install the client on a new device. Fetch the latest kind 30100 event for the target public key. Extract the PWKBlob and decrypt it with the passkey’s PRF value. Use the recovered private key for signing. Multiple passkeys can each have their own PWKBlob, allowing redundancy across devices.

Installation

Install from npm:

npm install ns-auth-sdk
# or
pnpm install ns-auth-sdk
# or
yarn add ns-auth-sdk

Quick Start

1. Initialize Services

import { AuthService, RelayService } from 'ns-auth-sdk';
import { EventStore } from 'ns-auth-sdk';

// Initialize auth service
const authService = new AuthService({
  rpId: 'your-domain.com',
  rpName: 'Your App Name',
  storageKey: 'nsauth_keyinfo',
  cacheOnCreation: true, // Enable early caching (default: true)
});

// Initialize relay service with EventStore
const relayService = new RelayService({
  relayUrls: ['wss://relay.io'],
});

// Initialize with EventStore
const eventStore = new EventStore(/* config */);
relayService.initialize(eventStore);

2. Create a Passkey

// Create a passkey (triggers biometric)
const credentialId = await authService.createPasskey('user@example.com');

// Create Key
const keyInfo = await authService.createKey(credentialId, undefined, {
  username: 'user@example.com',
  recoveryPassword: 'my-recovery-password', // optional
});

// Store keyInfo for later use
authService.setCurrentKeyInfo(keyInfo);

Password Fallback (when PRF unavailable)

When PRF is not supported, username is required and password is used to derive the key:

// Check if password fallback is needed
const prfSupported = await authService.checkPRFSupport();

if (!prfSupported) {
  // Username is REQUIRED when PRF unavailable
  const keyInfo = await authService.createKey(undefined, userPassword, {
    username: 'user@example.com',
  });
}

// Sign events
const signedEvent = await authService.signEvent(event);

API Reference

Methods

  • createPasskey(username?: string): Promise<Uint8Array> - Create a new passkey
  • createKey(credentialId?: Uint8Array, password?: string, options?: KeyOptions): Promise<KeyInfo> - Create key from passkey (auto-detects PRF support, uses password fallback if needed)
  • getPublicKey(): Promise<string> - Get current public key
  • signEvent(event: Event): Promise<Event> - Sign an event
  • getCurrentKeyInfo(): KeyInfo | null - Get current key info
  • setCurrentKeyInfo(keyInfo: KeyInfo): void - Set current key info
  • hasKeyInfo(): boolean - Check if key info exists
  • clearStoredKeyInfo(): void - Clear stored key info
  • checkPRFSupport(): Promise<boolean> - Check if PRF is supported
  • deriveSaltFromUsername(username?: string): Promise<string> - Derive salt from username (SHA-256)

Recovery Methods

  • addPasswordRecovery(password: string): Promise<KeyInfo> - Add password recovery to an existing PRF key
  • activateWithPassword(password: string, newCredentialId: Uint8Array): Promise<KeyInfo> - Recover using password with a new passkey credential ID from a new device
  • getRecoveryForKind0(): RecoveryData | null - Get recovery data for publishing to kind-0
  • parseRecoveryTag(tags: string[][]): KeyRecovery | null - Parse recovery tag from event
  • verifyRecoverySignature(kind0: Event): Promise<boolean> - Verify recovery signature (async)

KeyOptions

interface KeyOptions {
  username?: string;           // Required when PRF unavailable
  password?: string;           // Required when PRF unavailable
  recoveryPassword?: string;   // Password for recovery (optional)
}

RecoveryData

interface RecoveryData {
  recoveryPubkey: string;
  recoverySalt: string;
  createdAt?: number;
  signature?: string; // Schnorr signature from recovery key signing the current pubkey
}

Types

// KeyInfo with optional recovery
interface KeyInfo {
  credentialId: string;
  pubkey: string;
  salt: string;
  username?: string;
  recovery?: KeyRecovery;
}

// Key recovery configuration
interface KeyRecovery {
  recoveryPubkey: string;
  recoverySalt: string;
  createdAt?: number;
  signature?: string; // Schnorr signature from recovery key signing the current pubkey
}

// Sign options
interface SignOptions {
  clearMemory?: boolean;
  tags?: string[][];
  password?: string;
}

Recovery Flow

The SDK supports password-based recovery for passkey-protected keys. When creating a key, you can optionally provide a recovery password:

// Create key with recovery enabled
const keyInfo = await authService.createKey(credentialId, undefined, {
  username: 'user@example.com',
  recoveryPassword: 'my-recovery-password',
});

// The recovery data is stored in kind-0 tags:
// ["r", recoveryPubkey, recoverySalt, createdAt, signature]

Recovery on a new device:

// On new device - create new passkey first
const newCredentialId = await authService.createPasskey('user@example.com');

// Recover using password
const keyInfo = await authService.activateWithPassword('my-recovery-password', newCredentialId);

Verification:

Anyone can verify ownership by fetching the kind-0 and checking the signature:

import { parseRecoveryTag, verifyRecoverySignature } from 'ns-auth-sdk';

// Fetch kind-0 from relay
const kind0 = await relayService.fetchProfile(pubkey);

// Verify recovery signature (async)
const isValid = await verifyRecoverySignature(kind0);
if (isValid) {
  console.log('Recovery key holder controls this identity');
}

Configuration Options

interface KeyManagerOptions {
  cacheOptions?: {
    enabled: boolean;
    timeoutMs?: number;
    cacheOnCreation?: boolean; // Cache key immediately after derivation (default: true)
  };
  storageOptions?: {
    enabled: boolean;
    storage?: Storage;
    storageKey?: string;
  };
  prfOptions?: {
    rpId?: string;
    timeout?: number;
    userVerification?: UserVerificationRequirement;
  };
}

Cache Options:

  • enabled: Enable/disable key caching
  • timeoutMs: Cache timeout in milliseconds (default: 30 minutes)
  • cacheOnCreation: When true, caches the key immediately after createKey() to reduce biometric prompts from 2-3 to 1-2. This is enabled by default for better user experience.

RelayService

Service for communicating with relays.

Methods

  • initialize(eventStore: EventStore): void - Initialize with EventStore
  • getRelays(): string[] - Get current relay URLs
  • setRelays(urls: string[]): void - Set relay URLs
  • publishEvent(event: Event, timeoutMs?: number): Promise<boolean> - Publish event
  • fetchProfile(pubkey: string): Promise<ProfileMetadata | null> - Fetch profile
  • fetchProfileRoleTag(pubkey: string): Promise<string | null> - Fetch role tag
  • fetchFollowList(pubkey: string): Promise<FollowEntry[]> - Fetch follow list
  • fetchMultipleProfiles(pubkeys: string[]): Promise<Map<string, ProfileMetadata>> - Fetch multiple profiles
  • queryProfiles(pubkeys?: string[], limit?: number): Promise<Map<string, ProfileMetadata>> - Query profiles
  • publishFollowList(pubkey: string, followList: FollowEntry[], signEvent: (event: Event) => Promise<Event>): Promise<boolean> - Publish follow list

Integration with Applesauce

This library is designed to work seamlessly with applesauce-core. The RelayService uses applesauce's EventStore for all relay operations. All applesauce-core exports are re-exported from ns-auth-sdk for convenience.

import { EventStore } from 'ns-auth-sdk';
import { RelayService } from 'ns-auth-sdk';

const eventStore = new EventStore({
  // configuration
});

const relayService = new RelayService();
relayService.initialize(eventStore);

Event Support

The SDK helpers for building events:

import { 
  Helpers, 
  finalizeEvent, 
  getPublicKey, 
  generateSecretKey 
} from 'ns-auth-sdk';

// Generate a new key pair
const sk = generateSecretKey();
const pubkey = getPublicKey(sk);

// Create and sign an event
const event = finalizeEvent({
  kind: 0,
  content: 'My Event',
  created_at: Math.floor(Date.now() / 1000),
  tags: [],
}, sk);

Security Guidance

  • Configure a strict Content Security Policy (CSP) in the host app to restrict script and image sources.
  • Add rate limiting or debouncing around profile queries and event publishing in the host app or API layer.
  • Avoid surfacing raw error details to end users; log detailed errors in secure logs.

About

Decentralized SSO (beta): Authentication, membership and profile management

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors