# LiveFolio Agent Authentication (auth.md)

Welcome, AI Agent! This document describes how you can autonomously register accounts and authenticate with LiveFolio on behalf of users.

**Base URL:** `https://livefolio.cloud` (or your deployed instance)
**MCP Endpoint:** `POST /api/mcp` (JSON-RPC 2.0)

---

## Supported Flows

| Flow | Use Case | Auth Method |
|------|----------|-------------|
| **Agent Verified (ID-JAG)** | Hosted LLM platforms (ChatGPT, Claude) — instant, no user interaction | JWT with email claim |
| **User Claimed (OTP)** | CLI agents (Claude Code, Cursor) — user verifies via email | 6-digit code ceremony |

---

## Flow 1: Agent Verified (ID-JAG) — Instant API Key

Use this when you can construct a JWT with the user's email. No OTP. No user interaction. You get an API key back immediately.

### Step 1: Construct the JWT

Create an **unsigned** JWT (alg: none) with these claims:

```json
{
  "email": "user@example.com",
  "sub": "user@example.com",
  "iss": "your-agent-name",
  "iat": 1719705600,
  "aud": "livefolio"
}
```

Encode it as `base64(header).base64(payload).` (note trailing dot, no signature).

```bash
# Bash example:
HEADER=$(echo -n '{"alg":"none","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
PAYLOAD=$(echo -n '{"email":"user@example.com","sub":"user@example.com","iss":"my-agent","iat":'$(date +%s)',"aud":"livefolio"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
JWT="${HEADER}.${PAYLOAD}."
```

### Step 2: Send the assertion

```http
POST /api/agent/auth
Content-Type: application/json

{
  "type": "identity_assertion",
  "assertion": "eyJhbGciOiJub25lIn0.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.",
  "requested_credential_type": "api_key"
}
```

### Step 3: Receive your API key

```json
{
  "status": "completed",
  "credential": {
    "access_token": "lf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "token_type": "Bearer",
    "organization": {
      "id": "098c3b90-d1d3-4b68-9beb-52ecfbbe734f",
      "name": "user's Workspace",
      "plan": "Free"
    }
  }
}
```

---

## Flow 2: User Claimed (OTP) — Email Code Ceremony

Use this for local CLI agents where the user can check their email.

### Step 1: Start anonymous session

```http
POST /api/agent/auth
Content-Type: application/json

{
  "type": "anonymous",
  "requested_credential_type": "api_key"
}
```

Response:
```json
{
  "registration_session_id": "abc123...",
  "status": "pending_claim",
  "credential": {
    "token": "preclaim_token_here",
    "token_type": "APIKey",
    "expires_at": "2026-07-01T00:30:00.000Z"
  }
}
```

### Step 2: Trigger OTP

```http
POST /api/agent/auth/claim
Content-Type: application/json

{
  "email": "user@example.com",
  "registration_session_id": "abc123..."
}
```

The user receives a **6-digit code** in their email.

### Step 3: Ask the user for the code

Prompt: *"I've sent a 6-digit verification code to your email. Please enter it here."*

### Step 4: Complete the claim

```http
POST /api/agent/auth/claim/complete
Content-Type: application/json

{
  "email": "user@example.com",
  "code": "123456",
  "registration_session_id": "abc123..."
}
```

Response:
```json
{
  "status": "completed",
  "credential": {
    "access_token": "lf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "token_type": "Bearer",
    "expires_at": null,
    "organization": {
      "id": "098c3b90-d1d3-4b68-9beb-52ecfbbe734f",
      "name": "Workspace Name",
      "plan": "Free"
    }
  }
}
```

---

## Using the Credential

All MCP requests must include the API key as a Bearer token:

```http
POST /api/mcp
Content-Type: application/json
Authorization: Bearer lf_live_your_token_here

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
```

**Token format:** `lf_live_` prefix, 48 hex characters.
**Token storage:** Persist the token. It survives server restarts and can be reused.

---

## Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| `401 Unauthorized` | Missing or invalid Bearer token | Ensure `Authorization: Bearer lf_live_...` header is set |
| `400 Missing type` | Request body missing `type` field | Set `"type": "identity_assertion"` or `"type": "anonymous"` |
| `400 Invalid assertion format` | JWT doesn't have 3 parts | Ensure base64 encoding is correct, trailing dot is present |
| `401 assertion verification failed` | JWT payload missing email | Include `"email"` or `"sub"` claim in payload |
| `410 session expired` | OTP preclaim session timed out | Restart from Step 1 (sessions expire after 15 minutes) |
