-
Notifications
You must be signed in to change notification settings - Fork 2
Authentication
Three flavors:
-
API keys (
nk_...) — for agents, scripts, and CLI-configured MCP clients (Claude Code, Cursor). Format:Authorization: Bearer nk_.... -
User JWTs — for humans on REST.
POST /auth/login→ token. -
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.
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.
Two endpoints:
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"
}'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 workspacesEvery 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.
API keys can carry a per-key rate_limit_per_minute. See Rate-Limiting.
Set expires_at at create time to auto-expire a key. Useful for short-lived
tokens you hand to a contractor's agent.
- 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_KEYsigns JWTs — set it toopenssl rand -hex 32in production and never commit it.
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).
-
/.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.
| 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 |
- Client hits
/.well-known/oauth-authorization-serverto learn the auth endpoints. - Client
POST /oauth/registerwith itsredirect_urisand gets back aclient_id. - Client opens the browser to
/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…&scope=mcp. - User logs in (Nakatomi email + password). If they're in multiple workspaces, they pick one.
- Nakatomi redirects the browser to
redirect_uri?code=…&state=…. - Client
POST /oauth/tokenwithgrant_type=authorization_code, thecode, the originalredirect_uri, and thecode_verifierthat hashes to thecode_challenge. Gets back:{ "access_token": "nk_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "nk_…", "scope": "mcp" } - Client uses
Authorization: Bearer <access_token>on every MCP call. - After ~1 hour, client
POST /oauth/tokenwithgrant_type=refresh_tokento rotate.
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.
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.
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.
Repository · Issues · MIT licensed · maintained by Matt Dula