Skip to content

Latest commit

 

History

History
392 lines (276 loc) · 12.3 KB

File metadata and controls

392 lines (276 loc) · 12.3 KB

Integration Guide

How to add amesh to your existing application. Each recipe is self-contained — pick the one that matches your setup.


Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                    PAIRING (one-time)                                │
│                                                                     │
│  Your Server (target) ◄──WS──► Relay ◄──WS──► Client (controller)  │
│  amesh listen                          amesh invite 482916          │
│                                                                     │
│  Both sides verify a 6-digit code, then exchange public keys.       │
│  Trust is one-way: controller → target. Relay can be shut down.     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                 RUNTIME (every request)                              │
│                                                                     │
│  Controller ────HTTP + AuthMesh header────► Target                  │
│  amesh.fetch()                   amesh.verify()                     │
│                                                                     │
│  One-way. No relay. Stateless headers. Target cannot call back.     │
└─────────────────────────────────────────────────────────────────────┘

Recipe 1: Protect an Express API

Replace Bearer token authentication with device-bound cryptographic identity.

Server setup

npm install @authmesh/sdk express
// server.ts
import express from 'express';
import { amesh } from '@authmesh/sdk';

const app = express();
app.use(express.json());

// Add amesh verification middleware — checks signature, timestamp, nonce, allow list
// Works with express.json(), express.text(), or no body parser at all.
app.use('/api', amesh.verify());

// Public endpoint (no auth)
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Protected endpoint
app.post('/api/orders', (req, res) => {
  res.json({
    message: 'Authenticated!',
    device: req.authMesh.deviceId,
    name: req.authMesh.friendlyName,
  });
});

app.listen(3000, () => console.log('Server on :3000'));

Client setup

npm install @authmesh/sdk
// client.ts
import { amesh } from '@authmesh/sdk';

const res = await amesh.fetch('http://localhost:3000/api/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 100 }),
});

console.log(await res.json());
// { message: 'Authenticated!', device: 'am_...', name: 'My Laptop' }

Initial setup (run once per machine)

# Install the CLI
npm install -g @authmesh/cli

# On the server (target): create identity
amesh init --name "prod-api"

# On your laptop (controller): create identity
amesh init --name "my-laptop"

# Start the relay (needed only for pairing)
bunx @authmesh/relay

# On the server (target): start listening for pairing
amesh listen
# ✔ "my-laptop" added as controller.

# On your laptop (controller): pair with the server
amesh invite 482916
# ✔ "prod-api" added as target.

# Trust is one-way: laptop → server. The relay can be stopped now.

Pairing Remote Machines

When your server is remote (cloud VM, EC2, etc.), both machines need to reach the same relay.

Option A: Use the public relay (easiest)

amesh provides a free relay at relay.authmesh.dev:

# On the remote server (target — SSH in)
amesh listen --relay wss://relay.authmesh.dev/ws

# On your laptop (controller)
amesh invite 482916 --relay wss://relay.authmesh.dev/ws

Option B: Run the relay on the remote server

# On the remote server (target)
bunx @authmesh/relay                                      # starts on port 3001
amesh listen --relay ws://localhost:3001/ws

# On your laptop (controller — use the server's public IP or domain)
amesh invite 482916 --relay ws://your-server:3001/ws

Option C: Bootstrap token (non-interactive)

For servers where you can't run interactive commands:

# On your laptop — generate a token
amesh provision --name "prod-server" --ttl 1h
# Outputs: AMESH_BOOTSTRAP_TOKEN=eyJ...

# Set the token as an env var on the remote server, then run your app.
# The SDK auto-pairs on first request.

Self-hosted relay

For production, you should host your own relay. See the Self-Hosting Guide for Docker, Cloud Run, Fly.io, Kubernetes, and plain Node.js deployment options.


Recipe 2: Microservices (Service A calls Service B)

Each service gets its own device identity. Services pair once, then authenticate every request. Trust is one-way: the caller (controller) authenticates to the API (target), not vice versa.

Service B — the target (the API being called)

import express from 'express';
import { amesh } from '@authmesh/sdk';

const app = express();
app.use(express.json());
app.use(amesh.verify());

app.get('/internal/users/:id', (req, res) => {
  console.log(`Request from ${req.authMesh.friendlyName} (${req.authMesh.deviceId})`);
  res.json({ id: req.params.id, name: 'Alice' });
});

app.listen(4000);

Service A — the controller (the caller)

import { amesh } from '@authmesh/sdk';

async function getUser(id: string) {
  const res = await amesh.fetch(`http://service-b:4000/internal/users/${id}`);
  return res.json();
}

Setup for each service

# On service-b machine (target — the API):
amesh init --name "service-b"
amesh listen

# On service-a machine (controller — the caller):
amesh init --name "service-a"
amesh invite 482916

# One-way trust: service-a → service-b.
# service-b's allow list has service-a as [controller].
# service-b cannot authenticate back to service-a.

Bidirectional auth: If two services need to call each other, pair them twice — each side runs amesh listen once and amesh invite once. Each pairing creates a separate one-way trust relationship.


Recipe 3: Redis Nonce Store (Production Multi-Instance)

If you run multiple instances of your server (behind a load balancer), use Redis to share the nonce store. Without this, a replay attack could succeed by hitting a different instance.

npm install @authmesh/sdk ioredis
import express from 'express';
import { amesh } from '@authmesh/sdk';
import { RedisNonceStore } from '@authmesh/sdk/redis';

const app = express();
app.use(express.json());

app.use(amesh.verify({
  nonceStore: new RedisNonceStore(process.env.REDIS_URL),
}));

app.post('/api/orders', (req, res) => {
  res.json({ device: req.authMesh.deviceId });
});

app.listen(3000);

If you don't provide a nonceStore, amesh uses an in-memory store and prints a warning in production.


Recipe 4: Webhook Authentication

Instead of signing webhooks with a shared secret (like WEBHOOK_SECRET), sign them with amesh.

Sending webhooks

import { amesh } from '@authmesh/sdk';

async function sendWebhook(url: string, event: object) {
  const res = await amesh.fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(event),
  });
  return res.ok;
}

Receiving webhooks

import express from 'express';
import { amesh } from '@authmesh/sdk';

const app = express();
app.use(express.text({ type: '*/*' }));

// Only accept webhooks from paired devices
app.post('/webhooks', amesh.verify(), (req, res) => {
  const event = JSON.parse(req.body);
  console.log(`Webhook from ${req.authMesh.friendlyName}:`, event);
  res.sendStatus(200);
});

No shared secret. The webhook sender proves its identity with a device-bound signature.


Local Development

Use the same amesh.fetch() and amesh.verify() code paths in local development as in production. No if (NODE_ENV === 'development') { skipAuth() } hacks.

Setup

Use the encrypted-file backend for local services (passphrase auto-generated):

# Create identities for local services
AUTH_MESH_DIR=/tmp/amesh-a amesh init --name "local-service-a" --backend encrypted-file
AUTH_MESH_DIR=/tmp/amesh-b amesh init --name "local-service-b" --backend encrypted-file

# Start the relay (needed only for pairing)
bunx @authmesh/relay

# Pair them (same as production)
AUTH_MESH_DIR=/tmp/amesh-b amesh listen                  # on service-b (target)
AUTH_MESH_DIR=/tmp/amesh-a amesh invite 482916           # on service-a (controller)

Same code in dev and production

Your application code is identical everywhere:

// This is the SAME code in dev and production. No environment checks.
import { amesh } from '@authmesh/sdk';

const res = await amesh.fetch('http://localhost:4000/api/data', {
  method: 'POST',
  body: JSON.stringify({ query: 'test' }),
});

The only difference is the key storage backend:

  • Production: macOS Keychain, Secure Enclave, or TPM 2.0
  • Local dev: --backend encrypted-file (passphrase auto-generated)

Tips

  • Use AUTH_MESH_DIR to isolate each service's identity directory.
  • For Docker Compose, mount AUTH_MESH_DIR as a volume so identities (and their auto-generated passphrases) persist across restarts.
  • The relay is only needed during initial pairing. Once devices are paired, stop it.

Environment Variables

Variable Description Default
AUTH_MESH_DIR Directory for identity and keys ~/.amesh/
AUTH_MESH_PASSPHRASE Override auto-generated passphrase for encrypted-file backend (rarely needed) (optional)
AMESH_BOOTSTRAP_TOKEN Bootstrap token for automated pairing (optional)
AMESH_RELAY_URL WebSocket relay URL wss://relay.authmesh.dev/ws
REDIS_URL Redis URL for nonce store (optional)

TypeScript Types

// Available on req.authMesh after amesh.verify()
interface AuthMeshIdentity {
  deviceId: string;       // e.g. "am_cOixWcOdI8-pLh4P"
  friendlyName: string;   // e.g. "prod-api"
  verifiedAt: number;     // Unix timestamp of verification
}

// amesh.verify() options
interface VerifyOptions {
  clockSkewSeconds?: number;    // Default: 30
  nonceWindowSeconds?: number;  // Default: 60
  nonceStore?: NonceStore;      // Default: InMemoryNonceStore
}

Troubleshooting

"unauthorized" on every request

  1. Devices not paired? Run amesh list on the server — the client's device ID must be in the allow list.
  2. Clock skew? Server and client clocks must be within 30 seconds. Check with date on both machines.
  3. Body mismatch? The middleware handles express.json(), express.text(), and raw streams automatically. If you use a custom body parser that transforms the body (e.g., XML parsing, decompression), ensure the original body is preserved.

"allow_list_integrity_failure" (500)

The allow list file (~/.amesh/allow_list.json) was modified outside of amesh. This is the HMAC seal detecting tampering. Re-pair the devices to regenerate the file.

"Using in-memory nonce store" warning

You're running in production without a Redis nonce store. Replay attacks could succeed by hitting different instances. See Recipe 3 above.

"Keys are software-protected (no hardware keystore detected)"

amesh prefers hardware-backed storage (Secure Enclave, macOS Keychain, TPM 2.0). When none is available, it falls back to the encrypted-file backend automatically with an auto-generated passphrase.

To upgrade to hardware-backed storage:

  • macOS: Ensure the Swift helper binary is installed alongside the amesh binary for Keychain/Secure Enclave support.
  • Linux: Install tpm2-tools for TPM 2.0 support.

Then re-run amesh init --force to regenerate with the hardware backend.