# Authentication

> **TL;DR for agents:** Same `/api/*` endpoint works for both: `Authorization: Bearer ldm_pk_<rest>` for agents and MCP clients, or a Cookie/JWT session from `/login` for the web UI. Any auth failure returns the same generic `401 {"statusCode":401,"message":"Invalid credentials"}` — by design.

## UI = MCP — single surface

Every protected endpoint accepts either authentication scheme. Whatever the LDM web app does, an autonomous agent (Claude Desktop, Cursor, custom MCP client, n8n, server-to-server integration) can do via the same URL — just swap the JWT cookie for a Bearer token. There is no parallel `/v1/*` mirror to keep in sync.

Internally a single `HybridAuthGuard` resolves either method to a uniform `req.user` + `req.tenantId` so controller code is identical for both callers. Per-method `@RequireScope(...)` enforces fine-grained scope checks for Bearer keys; UI sessions get the scopes their role grants.

## Bearer token (agents / MCP / SDK)

Tokens are issued by `POST /v1/signup` and look like `ldm_pk_<random>`. Send them in the `Authorization` header on every request; scopes are checked against the key.

```bash
Authorization: Bearer ldm_pk_a1b2c3d4...
```

Use Bearer for: AI agents (Claude Desktop, Cursor), MCP servers, scheduled scripts, server-to-server integrations. The agent-card at `/.well-known/agent-card.json` advertises this scheme so discovery-aware clients can find the auth requirements without prior config.

## Cookie / JWT session (web UI)

Visit `https://app.live-direct-marketing.online/login` and authenticate with email + password. The browser receives an HttpOnly cookie carrying a short-lived JWT; subsequent `/api/*` requests use it automatically (no manual header).

```bash
# After /login completes, the UI calls the API like this — same endpoints,
# auth comes from the session cookie set by /login.
curl -b cookies.txt https://api.live-direct-marketing.online/api/companies
```

Use Cookie/JWT for: the LDM web app itself, manual exploration in a browser, local dev. Roles `OWNER` and `ADMIN` carry all scopes; other roles get scopes per their assignment in the workspace.

## Generic 401 (anti-enumeration)

A missing header, malformed scheme, unknown key, or revoked key — all four failure modes return the same response. Distinct error messages would let attackers enumerate valid key prefixes or formats.

```json
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "statusCode": 401,
  "message": "Invalid credentials"
}
```

If you receive this response, verify (in order): (1) the `Authorization` header is sent at all, (2) the value starts with `Bearer ` (with the trailing space), (3) the key was copied verbatim from `/v1/signup` response, (4) the key has not been revoked.

## Server-side storage

Plaintext keys are returned by `/v1/signup` exactly once. The server stores only a SHA-256 hash with a 7-character prefix for fast lookup. The plaintext is never logged.
