Onboarding a Regular User
How a regular user authorizes a device from an invitation and stores the permanent session credentials returned by the server.
Complete Setup and Installation before following this guide.
User types
Blockstream Enterprise Custody supports two user types:
| Type | Authentication model | Typical use |
|---|---|---|
| Regular user | Invitation plus device auth | Human operators using a dashboard or client app |
| API user | Direct key-based onboarding | Programmatic integrations and backend services |
This page covers regular-user device onboarding. For machine-oriented onboarding, see API User Onboarding Workflow.
What this flow does
In the invitation flow:
- an admin invites the user
- the user receives temporary invitation credentials
- the user generates permanent session keys locally
- the user authorizes the device with the invitation
- the server returns the permanent device identity
Before you begin
This guide uses:
@blockstream/cryptic@blockstream/cryptic/utilsuuid
You also need:
- an invitation payload from the server
- the one-time ECDSA private key from that invitation flow
- a
Broadcasterfor the invitation session, or one of the lower-level request helpers from Making Requests
If your deployment requires mTLS for /request, keep the onboarding sequence here and replace only the broadcaster transport with the Node-side mTLS flow from Making Requests.
What comes in the invitation
| Field | Meaning |
|---|---|
invite_string | One-time server-issued invitation token |
oneTimePrivateKey | One-time ECDSA signing key for the authorization request |
deviceUuid | Temporary device identifier bound to the invitation |
server_rsa_pub_key_pem_b64 | Base64-encoded server RSA public key |
server_ecdsa_pub_key | Server ECDSA public key as hex |
Step 1: Generate permanent session keys
Generate the keys that the device will keep after onboarding:
import { generateUserKeyPairs } from '@blockstream/cryptic/utils'
const sessionKeys = await generateUserKeyPairs()Step 2: Decode the server RSA key
If the invitation contains the RSA public key as base64-encoded PEM, decode it before constructing the client:
const serverRsaPublicKeyPem = Buffer.from(invitation.server_rsa_pub_key_pem_b64, 'base64').toString(
'utf-8',
)Step 3: Create the one-time authorization client
The authorization request is signed with the invitation's one-time ECDSA key, but it should use the new device RSA key so the server can return encrypted data for the permanent device session.
import { Buffer } from 'buffer'
import { Blockstream } from '@blockstream/cryptic'
import { Broadcaster } from '@blockstream/cryptic/broadcaster'
const oneTimeClient = new Blockstream(
sessionKeys.rsa.privateKeyPem,
Buffer.from(invitation.oneTimePrivateKey, 'hex'),
invitation.deviceUuid,
serverRsaPublicKeyPem,
Buffer.from(invitation.server_ecdsa_pub_key, 'hex'),
)
const oneTimeBroadcaster = new Broadcaster(ECS_BASE_URL, oneTimeClient)Step 4: Authorize the device
import { v4 as uuidv4 } from 'uuid'
const response = await oneTimeBroadcaster.processBroadcast({
action: 'add',
resource: '/account-management/authorize',
details: {
invite_string: invitation.invite_string,
device_name: `${uuidv4()}-device`,
device_signature_pubkey: sessionKeys.ecdsa.publicKeyHex,
device_encryption_pubkey: sessionKeys.rsa.publicKeyPem,
password,
},
})If your environment uses passwords for user devices, pass it here. Otherwise leave it empty or follow your backend policy.
In mTLS-enabled deployments, this authorization request is still the same ECS message. Only the broadcaster transport changes, using the mTLS pattern from Making Requests.
Step 5: Store the permanent session credentials
On success, persist the final server response:
if (response.status !== 'success') {
throw new Error(response.message ?? 'Authorization failed')
}
const session = {
userId: response.details.user_id,
deviceUuid: response.details.device.device_uuid,
sessionPrivateKeys: {
ecdsa: sessionKeys.ecdsa.privateKeyHex,
rsa: sessionKeys.rsa.privateKeyPem,
},
serverPublicKeys: {
rsa: serverRsaPublicKeyPem,
ecdsa: invitation.server_ecdsa_pub_key,
},
}The returned device_uuid is now the permanent device identity to use for future authenticated requests.
Proposal state
If the authorization returns status: "pending", the request became a proposal and must be approved before the device becomes active.
if (response.status === 'pending') {
console.log('Awaiting approval for proposal:', response.details.proposal_id)
}