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.
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.
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.
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.
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.
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).
import {loginWithPubkey} from "@welshman/signer"
// Log in as hodlbod
loginWithPubkey("97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322")
Using the current session
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)
.