Skip to content

Authentication

Matt Dula edited this page Apr 18, 2026 · 2 revisions

Authentication

Three flavors:

  1. API keys (nk_...) — for agents, scripts, and CLI-configured MCP clients (Claude Code, Cursor). Format: Authorization: Bearer nk_....
  2. User JWTs — for humans on REST. POST /auth/login → token.
  3. OAuth 2.1 — for MCP clients that speak OAuth (Claude Desktop's "Add Custom Connector" GUI, ChatGPT Custom Connectors). The client runs a PKCE authorization-code flow against our /oauth/* endpoints in a browser and stores the resulting tokens. No copy-paste. See §OAuth 2.1 below.

API keys — the agent path

Format: nk_<prefix>_<secret> (stored as SHA-256 hash; the plaintext is shown exactly once at creation).

Create:

curl -X POST http://localhost:8000/workspace/api-keys \
  -H "Authorization: Bearer <jwt-or-existing-key>" \
  -H "Content-Type: application/json" \
  -d '{"name":"sdr-agent","role":"member","rate_limit_per_minute":120}'

Response includes "key": "nk_..."save it now. There is no recovery flow. A new key mints in about a second if you lose one, but the lost one has to be revoked.

Use:

Authorization: Bearer nk_<prefix>_<secret>

On every request. The workspace is inferred from the key — no X-Workspace header needed.

Revoke:

curl -X DELETE http://localhost:8000/workspace/api-keys/<key-id> \
  -H "Authorization: Bearer <admin-key>"

This sets revoked_at; subsequent requests with the old key get 401.

User JWTs — the human path

Two endpoints:

Signup (creates workspace + owner)

curl -X POST http://localhost:8000/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "password": "correct-horse-battery-staple",
    "workspace_name": "Acme",
    "workspace_slug": "acme"
  }'

Login (for existing users)

curl -X POST http://localhost:8000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"..."}'

Both return:

{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "user_id": "...",
  "workspace_id": "...",
  "workspace_slug": "acme",
  "expires_in_seconds": 3600
}

Use:

Authorization: Bearer <jwt>
X-Workspace: acme         # slug or id; required when the token covers multiple workspaces

Roles

Every membership and every API key carries a role:

Role Can do
owner everything, including revoke other owners
admin everything except revoke the last owner
member read + write CRM; cannot mint API keys or edit workspace config
readonly read only

Assign your SDR agents member. Never give an agent owner.

Rate limits

API keys can carry a per-key rate_limit_per_minute. See Rate-Limiting.

Expiry

Set expires_at at create time to auto-expire a key. Useful for short-lived tokens you hand to a contractor's agent.

Security considerations

  • Keys are stored as SHA-256 digests, never plaintext.
  • Webhook secrets (separate from API keys) are random 64-char hex, also generated server-side and shown once.
  • Passwords use bcrypt (pinned to 4.0.1 for passlib compatibility).
  • SECRET_KEY signs JWTs — set it to openssl rand -hex 32 in production and never commit it.

OAuth 2.1

Nakatomi speaks OAuth 2.1 over HTTP for MCP clients that expect it (Claude Desktop's connector GUI, ChatGPT Custom Connectors, anything else following the MCP auth spec).

Discovery

  • /.well-known/oauth-authorization-server — RFC 8414 metadata
  • /.well-known/oauth-protected-resource — RFC 9728 metadata

MCP clients usually fetch the first endpoint automatically when you hand them your server URL.

Endpoints

Route Purpose
POST /oauth/register RFC 7591 dynamic client registration (public clients only — PKCE)
GET /oauth/authorize Login + consent HTML
POST /oauth/authorize Form submit → redirect with code
POST /oauth/token Exchange code for tokens; or refresh_token grant
POST /oauth/revoke RFC 7009 revocation

The flow (what your MCP client does)

  1. Client hits /.well-known/oauth-authorization-server to learn the auth endpoints.
  2. Client POST /oauth/register with its redirect_uris and gets back a client_id.
  3. Client opens the browser to /oauth/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…&scope=mcp.
  4. User logs in (Nakatomi email + password). If they're in multiple workspaces, they pick one.
  5. Nakatomi redirects the browser to redirect_uri?code=…&state=….
  6. Client POST /oauth/token with grant_type=authorization_code, the code, the original redirect_uri, and the code_verifier that hashes to the code_challenge. Gets back:
    { "access_token": "nk_…", "token_type": "Bearer", "expires_in": 3600,
      "refresh_token": "nk_…", "scope": "mcp" }
  7. Client uses Authorization: Bearer <access_token> on every MCP call.
  8. After ~1 hour, client POST /oauth/token with grant_type=refresh_token to rotate.

Scopes

Single scope for v1: mcp. Grants full workspace access at the user's membership role (owner, admin, member, readonly). Finer-grained scopes are a v2 roadmap item.

Token storage

Access and refresh tokens are stored as ApiKey rows. Same format as manually-minted keys — same get_principal validation path. Access tokens carry a 1-hour expires_at; refresh tokens carry 30 days. Every refresh rotates both.

Revoking

POST /oauth/revoke with token=<access_or_refresh>. Also applies when the user deletes the corresponding row through DELETE /workspace/api-keys/<id> — tokens are just API keys.

See also: SECURITY.md.

Clone this wiki locally