Skip to content

Session Management

The session system provides a unified way to handle different authentication methods:

  • NIP-01 via Secret Key
  • NIP-07 via Browser Extension
  • NIP-46 via Bunker URL or Nostrconnect
  • NIP-55 via Android Signer Application
  • Read-only pubkey login

Overview

Sessions are stored in local storage and can be:

  • Persisted across page reloads
  • Used with multiple accounts
  • Switched dynamically
  • Backed by different signing methods

NIP 01 Example

The simplest type of login is NIP 01, although it's generally a bad idea to be handling user keys. NIP 46, 44, or 07 login are preferable. However, NIP 01 can be useful for supporting signup, local profiles, or ephemeral keys.

typescript
import {makeSecret} from '@welshman/signer'
import {loginWithNip01} from '@welshman/app'

loginWithNip01(makeSecret())

NIP 07 Example

A simple way to sign in for desktop browser users is using NIP 07. This method is easy to implement, but should be used sparingly, since not all users will be using a browser with a nostr signing extension installed.

typescript
import {Nip07Signer} from '@welshman/signer'
import {loginWithNip07} from '@welshman/app'

const signer = new Nip07Signer()

signer.getPubkey().then(pubkey => {
  if (pubkey) {
    loginWithNip07(pubkey)
  } else {
    // User extension does not exist or did not respond
  }
})

NIP-46 Authentication

The best default signing scheme is NIP 46, AKA "Nostr Connect". This supports multiple handshakes depending on desired UX, and can support advanced use cases like secure enclaves, self-hosted keys, and FROST multisig.

The simpler bunker:// handshake is done by asking the user to provide a bunker URL, either by QR code, or by pasting it manually into your application.

typescript
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {loginWithNip46, nip46Perms} from "@welshman/app"
import {isKeyValid} from "src/util/nostr"

// Make a client secret - this is distinct from the user's private key, and is used
// for communicating securely with the remote signer
const clientSecret = makeSecret()

// Ask the user to input their bunker URL
const bunkerUrl = prompt("Please enter your bunker url")

// Pase the bunker url
const {signerPubkey, connectSecret, relays} = Nip46Broker.parseBunkerUrl(bunkerUrl)

if (!isKeyValid(signerPubkey)) {
  alert("Sorry, but that's an invalid public key.")
} else if (relays.length === 0) {
  alert("That connection string doesn't have any relays.")
} else {
  // Open up a connection with the signer
  const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})

  // Send a connect request with the default permissions
  const result = await broker.connect(connectSecret, nip46Perms)

  // Make sure to check the connect secret to prevent hijacking
  if (result === connectSecret) {
    // Get the user's public key
    const pubkey = await broker.getPublicKey()

    if (!pubkey) {
      alert("Failed to initialize session")
    } else {
      loginWithNip46(pubkey, clientSecret, signerPubkey, relays)
    }
  }
}

Alternatively, you can provide the user with a nostrconnect:// URL which they can copy or scan with their signer. This is a better UX for users using a signer on their mobile phone.

typescript
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {loginWithNip46, nip46Perms} from "@welshman/app"

// Create a client secret
const clientSecret = makeSecret()

// Stop listening if the user cancels login
const abortController = new AbortController()

// Customize to use relays the signer can send responses to
const relays = ['wss://relay.nsec.app/']

// Create a broker
const broker = Nip46Broker.get({clientSecret, relays})

// Create a nostrconnect:// url
const nostrconnect = await broker.makeNostrconnectUrl({
  name: "My App",
  url: window.origin,
  image: window.origin + '/logo.png',
  perms: nip46Perms,
})

// Share it with the user. Displaying a QR code is particularly helpful
alert("To connect, paste this URL into your signer: " + nostrconnect)

// Listen for the response
let response
try {
  response = await broker.waitForNostrconnect(nostrconnect, abortController.signal)
} catch (errorResponse: any) {
  if (errorResponse?.error) {
    alert(`Received error from signer: ${errorResponse.error}`)
  } else if (errorResponse) {
    console.error(errorResponse)
  }
}

// If we got a response, the broker is already connected and we can log in
if (response) {
  const pubkey = await broker.getPublicKey()

  if (!pubkey) {
    alert("Failed to initialize session")
  } else {
    loginWithNip46(pubkey, clientSecret, response.event.pubkey, relays)
  }
}

NIP-55 Authentication

For the best UX on Android, use NIP 55. Note that this only works for web applications that have been compiled to native Android applications using CapacitorJS and nostr-signer-capacitor-plugin.

typescript
import {getNip55, Nip55Signer, loginWithNip55} from "@welshman/signer"

// Query for installed apps that implement nip 55 signing
getNip55().then(signerApps => {
  // We'll choose the first one and auto-login, but in most cases you'll want to offer a choice
  if (signerApps.length > 0) {
    const signer = new Nip55Signer(signerApps[0].packageName)
    const pubkey = await signer.getPubkey()

    if (pubkey) {
      loginWithNip55(pubkey, app.packageName)
    }
  }
})

Read-only session

A fun feature of nostr is that you can log in as other people, and see what nostr is like from their perspective (minus encrypted data or course).

typescript
import {loginWithPubkey} from "@welshman/signer"

// Log in as hodlbod
loginWithPubkey("97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322")

Using the current session

typescript
import {signer, session} from '@welshman/app'
import {createEvent, NOTE} from '@welshman/util'

// Print the current session - be aware the private key is stored in memory, be very
// careful about how you handle session objects!
console.log(session.get())

// Current session's signer is always ready to use
const event = await signer.get().sign(
  createEvent(NOTE, {content: "Hello Nostr!"})
)

// hodlbod's pubkey
const otherPubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"

// Encrypt content for private notes
const ciphertext = await signer.get().nip44.encrypt(otherPubkey, "Secret message")

// Decrypt automatically detects encryption version
const plaintext = await decrypt(signer, otherPubkey, ciphertext)

Multiple sessions

It's possible to support multiple concurrent sessions by simply calling addSession multiple times. This will update sessions, and set pubkey to the most recently added session. You can then switch between sessions by calling pubkey.set with a valid session pubkey, and delete sessions using dropSession(pubkey).