209 lines
7.0 KiB
Markdown
209 lines
7.0 KiB
Markdown
# 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](https://github.com/nostr-protocol/nips/blob/master/07.md). 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](https://github.com/nostr-protocol/nips/blob/master/46.md), 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](https://github.com/nostr-protocol/nips/blob/master/55.md). Note that this only works for web applications that have been compiled to native Android applications using [CapacitorJS](https://capacitorjs.com/) and [nostr-signer-capacitor-plugin](https://github.com/chebizarro/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)`.
|