← All lessons/Add-ons
58
Add-ons

User Auth — Register, Login & Magic Links

Add end-user authentication to your app: password registration, login, passwordless magic-link sign-in, JWT access tokens, and refresh token rotation.

Prerequisite: Lesson 34 complete

What you'll learn

  • ✓SAPIX_AUTH_ENABLED=true — enable the auth add-on (included in enterprise binary, no recompile needed)
  • ✓GET /v1/auth/status — returns { enabled: bool }, safe to call without an API key (feature probe)
  • ✓POST /v1/auth/register — email + password, returns JWT + refresh_token
  • ✓POST /v1/auth/login — password auth, same response shape
  • ✓POST /v1/auth/magic-link/send + /verify — passwordless email sign-in
  • ✓JWT is EdDSA (Ed25519), verified via GET /v1/auth/jwks — verify without contacting SapixDB
  • ✓POST /v1/auth/refresh — exchange refresh_token for a new JWT (30-day refresh window by default)
  • ✓POST /v1/auth/logout — delete refresh_token; POST /v1/auth/revoke — revoke JWT by jti
  • ✓Per-email brute-force lockout (5 failures → 15-min lock, configurable)
  • ✓GET /v1/auth/me — decode the current user from their Bearer JWT
Challenge

Register a user, log in, call GET /v1/auth/me. Expire the JWT by setting SAPIX_AUTH_JWT_EXPIRY_SECS=1, then call refresh and confirm you get a new valid JWT.

## User Auth — Register, Login & Magic Links

Lessons 34–35 covered machine-to-machine API keys. This lesson covers the User Auth add-on — a full end-user authentication system built directly into sapix-agent with no external auth service required.

> Prerequisite: Lesson 34 (API keys & scopes). This add-on runs alongside the API-key layer, not instead of it.

---

## Enable

SAPIX_AUTH_ENABLED=true
SAPIX_AUTH_JWT_SECRET=<32-byte hex>         # for JWT signing (EdDSA)
SAPIX_AUTH_MAGIC_LINK_TTL_SECS=900          # magic link expiry, default 15 min
SAPIX_AUTH_RESEND_API_KEY=re_...            # required for magic-link email delivery
SAPIX_AUTH_FROM_EMAIL=noreply@yourdomain.com

User records are stored on the agent's own strand — the same append-only, cryptographically signed chain as all other data.

---

## Registration

curl -X POST http://localhost:7475/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "password": "correct-horse-battery"
  }'

Response: `json { "user_id": "usr_a3f9...", "email": "alice@example.com", "created_at": "2026-08-19T..." } `

Passwords are hashed with Argon2id before storage. The raw password never appears in any strand record.

---

## Login (password)

curl -X POST http://localhost:7475/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{ "email": "alice@example.com", "password": "correct-horse-battery" }'

Response: `json { "access_token": "eyJ...", "refresh_token": "spx_rt_...", "expires_in": 3600 } `

The access_token is a short-lived EdDSA JWT (1 hour default). The refresh_token is a long-lived opaque token stored on the strand — revocable at any time.

---

## JWT structure

SapixDB issues EdDSA (Ed25519) JWTs — the same keypair that signs strand records also signs your user tokens.

{
  "sub": "usr_a3f9...",
  "email": "alice@example.com",
  "app_metadata": { "role": "admin" },
  "user_metadata": { "display_name": "Alice" },
  "iat": 1755648000,
  "exp": 1755651600
}

Verify the public key at: ` GET /v1/auth/jwks `

This returns a standard JWKS document — compatible with any JWT library that supports JWKS key discovery.

---

## Get the current user

curl http://localhost:7475/v1/auth/me \
  -H "Authorization: Bearer eyJ..."

Response: `json { "user_id": "usr_a3f9...", "email": "alice@example.com", "app_metadata": {}, "user_metadata": {}, "created_at": "2026-08-19T..." } `

---

## Refresh a JWT

Access tokens expire in 1 hour. Use the refresh token to get a new one without forcing the user to log in again:

curl -X POST http://localhost:7475/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refresh_token": "spx_rt_..." }'

Response: same structure as login — new access_token + new refresh_token.

The old refresh token is immediately invalidated (token rotation).

---

## Logout & token revocation

`bash # Revoke the current refresh token (logout from this device) curl -X POST http://localhost:7475/v1/auth/logout \ -H "Authorization: Bearer eyJ..." \ -H "Content-Type: application/json" \ -d '{ "refresh_token": "spx_rt_..." }'

# Revoke ALL refresh tokens for this user (logout from all devices) curl -X POST http://localhost:7475/v1/auth/revoke-all \ -H "Authorization: Bearer eyJ..." `

Revocation writes a tombstone on the strand — the token is gone even after a server restart.

---

## Magic links (passwordless)

Step 1 — Request a magic link: `bash curl -X POST http://localhost:7475/v1/auth/magic-link/send \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com" }' `

SapixDB generates a one-time token, stores it (TTL: 15 min), and sends an email via Resend with a link like: https://yourapp.com/auth/callback?token=otp_abc123

Step 2 — Your frontend exchanges the token: `bash curl -X POST http://localhost:7475/v1/auth/magic-link/verify \ -H "Content-Type: application/json" \ -d '{ "token": "otp_abc123" }' `

Response: same as login — access_token + refresh_token. The one-time token is invalidated immediately after use.

---

## Per-email brute-force lockout

After 5 consecutive failed login attempts for the same email address, that account is locked for 15 minutes. The lockout is stored on the strand — Alabay monitors it and will emit an alert finding if lockouts exceed a threshold.

The global IP rate limit (Lesson 35) handles cross-email scanning at the network layer.

---

## Full endpoint reference

MethodPathDescription
GET/v1/auth/statusReturns { enabled: bool } — no API key required
POST/v1/auth/registerCreate a new user account
POST/v1/auth/loginPassword login → JWT + refresh token
GET/v1/auth/meCurrent user profile (requires JWT)
POST/v1/auth/refreshExchange refresh token for new JWT
POST/v1/auth/logoutRevoke one refresh token
POST/v1/auth/revoke-allRevoke all refresh tokens for this user
POST/v1/auth/magic-link/sendSend a passwordless magic link
POST/v1/auth/magic-link/verifyVerify magic link token → JWT
GET/v1/auth/jwksEd25519 public key in JWKS format

---

## TypeScript: auth client

`typescript const BASE = 'http://localhost:7475';

async function login(email: string, password: string) { const res = await fetch(${BASE}/v1/auth/login, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise<{ access_token: string; refresh_token: string; expires_in: number }>; }

async function getMe(accessToken: string) { const res = await fetch(${BASE}/v1/auth/me, { headers: { Authorization: Bearer ${accessToken} }, }); return res.json(); }

async function refresh(refreshToken: string) { const res = await fetch(${BASE}/v1/auth/refresh, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken }), }); return res.json(); } `

---

## Challenge

  1. Register a new user at /v1/auth/register.
  2. Log in with /v1/auth/login and save both tokens.
  3. Call /v1/auth/me with the access token — confirm your email and user_id appear.
  4. Call /v1/auth/refresh to rotate the refresh token — verify you receive a new access token.
  5. Call /v1/auth/logout to revoke the refresh token, then try /v1/auth/refresh again — confirm it returns 401.

---

See also: Lesson 34 (API keys), Lesson 35 (global IP rate limiting), Lesson 59 (OAuth, passkeys, and custom claims).

← Previous
Lesson 57: Sherapd & Alabay Watchdogs
Next →
Lesson 59: OAuth, Passkeys & Custom Claims

The Sensart ecosystem

Explore our products

One team, many agents. Each product below is built by Sensart Technologies toward an agent-native future.

Boboyka VoiceBoboyka VoiceVoice generation engineHire BoboykaHire BoboykaHire 24/7 AI employeesBoboykaBoboykaAutonomous software factoryCodiosCodiosAgent caging platformA2AA2AAI agent governance platformConduitConduitVisual MCP orchestration studioPentestPentestAI penetration testingPromoterPromoterGrow your product organicallyMactebMactebGamified homeschooling
An umbrella ofSensart TechnologiesSensart Technologies