# auth.md

You are an agent. This document is the skill manifest for **Dex**, a personal CRM.
It tells you how to get a credential you can use against Dex's MCP server and its
REST API, in the order you should try things.

Dex publishes this file under the [auth.md](https://workos.com/auth-md) profile.
Read the ["What Dex does not implement"](#what-dex-does-not-implement) section
before you assume a step from that profile exists here — Dex implements the
**discovery** half of it, not the identity/claim ceremony half.

Two hosts matter:

- `https://mcp.getdex.com` — the MCP server **and** its OAuth authorization server.
- `https://api.prod.getdex.com` — the REST API. It is a **separate** resource with
  a **separate** credential type. See [Step 5](#step-5--the-rest-api-is-a-separate-resource).

---

## Step 1 — Discover

Discovery is two hops. If you already hold the `401` from a Dex MCP call, you have
done the first one.

Any data-plane call to `https://mcp.getdex.com/mcp` without a token answers:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.getdex.com/.well-known/oauth-protected-resource/mcp"
```

### 1a. Protected Resource Metadata (RFC 9728)

```http
GET https://mcp.getdex.com/.well-known/oauth-protected-resource/mcp
```

```json
{
  "resource": "https://mcp.getdex.com/mcp",
  "authorization_servers": ["https://mcp.getdex.com/"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["read", "write"],
  "resource_documentation": "https://getdex.com/docs/api-reference/authentication",
  "resource_name": "Dex Personal CRM API"
}
```

`authorization_servers[0]` is where you go next.

### 1b. Authorization Server Metadata (RFC 8414)

```http
GET https://mcp.getdex.com/.well-known/oauth-authorization-server
```

```json
{
  "issuer": "https://mcp.getdex.com/",
  "authorization_endpoint": "https://mcp.getdex.com/authorize",
  "token_endpoint": "https://mcp.getdex.com/token",
  "registration_endpoint": "https://mcp.getdex.com/register",
  "revocation_endpoint": "https://mcp.getdex.com/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "none"
  ],
  "scopes_supported": ["read", "write"],
  "service_documentation": "https://getdex.com/docs/ai/mcp-server",
  "agent_auth": {
    "skill": "https://getdex.com/auth.md",
    "register_uri": "https://mcp.getdex.com/register",
    "identity_types_supported": ["anonymous"],
    "anonymous": {
      "credential_types_supported": [
        "urn:ietf:params:oauth:token-type:access_token"
      ]
    }
  }
}
```

A byte-identical copy is served from the apex at
`https://getdex.com/.well-known/oauth-authorization-server` for clients that probe
the site root. Prefer the one the PRM named.

What the `agent_auth` block means here:

- `identity_types_supported: ["anonymous"]` — you register with **no asserted user
  identity**. There is no assertion for Dex to check, and none is required.
- `register_uri` — the RFC 7591 dynamic client registration endpoint. This is your
  entry point, and it is open: no pre-provisioned `client_id`, no partner
  agreement, no contact form.
- `anonymous.credential_types_supported` — what you end up holding: an OAuth 2.0
  bearer access token.
- `skill` — this document.

---

## Step 2 — Pick a method

| You are…                                                        | Use                                                         |
| --------------------------------------------------------------- | ----------------------------------------------------------- |
| An MCP client that can open a browser (or hand the user a URL)  | [OAuth + PKCE](#step-3--register-and-authorize-oauth--pkce) |
| Headless, but a human can visit a URL on another device         | [Device flow](#step-4--device-flow-headless-clients)        |
| A hosted agent with a shell but no browser (Grok Bot, OpenClaw) | [Device flow](#step-4--device-flow-headless-clients)        |
| A host that only accepts a URL + static header (xAI remote MCP) | [No-OAuth hosts](#hosts-with-no-oauth-support)              |
| Calling the REST API, or want a long-lived credential           | [API key](#step-5--the-rest-api-is-a-separate-resource)     |

### Hosts with no OAuth support

**If your host has no OAuth support at all, stop before Step 3.** Some MCP hosts
configure a server with nothing but a URL and a static `Authorization` header —
xAI's remote-MCP tool and Grok Bot's custom-connector form are the common
examples. They never follow a `WWW-Authenticate` challenge, so no authorize URL
is ever requested and the connector reports `authentication_required` with no
link to show the user. That is a property of the host, not a Dex failure.
Two things work there instead:

- **You can run shell commands** (Grok Bot, OpenClaw): install `@getdex/cli` and
  run the [device flow](#step-4--device-flow-headless-clients). This is the best
  path — the user signs in on their own device and you never handle their
  password.
- **You cannot run commands**: have the user mint a `dex_` key and paste it into
  the host's connector configuration field as `Authorization: Bearer dex_…` —
  never into the chat window, and never ask them to send it to you. A key
  does not expire and acts in one place: the user's Personal Dex, or the one
  workspace it was created for. Unless it was created Read-only, treat it as a
  full credential for that place
  (see [Step 5](#step-5--the-rest-api-is-a-separate-resource)). Tool calls will
  then work. The host's connection badge may still read "unauthenticated"
  because it tracks OAuth state it never had; that badge alone is cosmetic, but
  a `401` or `403` on a tool call is a genuine rejection — surface it rather
  than retrying the same credential.

There is **no anonymous data access**. Every tool call, resource read and prompt
fetch is scoped to one Dex user's private CRM, so every path below terminates in a
human signing in and consenting. What differs is how the human is reached.

Schema discovery is the one exception: `initialize`, `tools/list`, `resources/list`,
`prompts/list` and `ping` answer unauthenticated on `POST https://mcp.getdex.com/mcp`
so directories and validators can index the server. They return no user data.

---

## Step 3 — Register and authorize (OAuth + PKCE)

The standard path. Dex is a normal OAuth 2.1 authorization server; if your MCP
client already speaks OAuth, point it at `https://mcp.getdex.com/mcp` and it will
do all of this for you.

### 3a. Register (RFC 7591)

```http
POST https://mcp.getdex.com/register
Content-Type: application/json

{
  "client_name": "Your Agent",
  "redirect_uris": ["http://127.0.0.1:8976/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

You get back a `client_id` (and a `client_secret` if you asked for a confidential
client). Public clients should use `"token_endpoint_auth_method": "none"` and rely
on PKCE.

Registrations are held for 90 days of inactivity. If a later `/authorize` returns
`invalid_client`, re-register and retry — do not treat it as a permanent failure.

Loopback redirect URIs get RFC 8252 §7.3 treatment: the port may change between
runs of a CLI without re-registering, as long as scheme, host and path match.

### 3b. Authorize

```
GET https://mcp.getdex.com/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=<your redirect_uri>
  &code_challenge=<S256 challenge>
  &code_challenge_method=S256
  &scope=read%20write
  &state=<opaque>
```

`S256` is the only supported `code_challenge_method`. Send the user to this URL.
They sign in to Dex (email/password, Google, or a magic link) and land on an
explicit consent screen naming your client. **This is the consent gate** — surface
what you are asking for before you send them.

Scopes are `read` and `write`. Ask for `read` alone if you never mutate; the
consent screen shows the user what you requested.

### 3c. Exchange the code

```http
POST https://mcp.getdex.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=<same redirect_uri>
&client_id=<client_id>
&code_verifier=<PKCE verifier>
```

```json
{
  "access_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "<opaque>",
  "scope": "read write"
}
```

### 3d. Refresh

```http
POST https://mcp.getdex.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>
```

Refresh tokens rotate: each use issues a new pair and consumes the old one. If you
fan out parallel requests that share one stored refresh token, a short grace window
replays the same newly-issued pair rather than failing the losers — but store the
new token as soon as you get it.

On `invalid_grant`, discard the refresh token and restart at 3b.

---

## Step 4 — Device flow (headless clients)

For a CLI or a container with no browser. Shaped like RFC 8628, with one
difference you must not miss: **it issues a `dex_` API key, not an OAuth access
token.** Read [Step 5](#step-5--the-rest-api-is-a-separate-resource) for what that
credential is and where it works.

### 4a. Request a code

```http
POST https://mcp.getdex.com/device/code
User-Agent: your-agent/1.0
```

A `User-Agent` is required — an empty one is rejected at the edge before it
reaches Dex.

```json
{
  "device_code": "<hex>",
  "user_code": "ABCD-2345",
  "verification_uri": "https://getdex.com/app/auth/device-code",
  "expires_in": 600,
  "interval": 5
}
```

### 4b. Hand off to the user

Show them `verification_uri` and `user_code` together:

> Open https://getdex.com/app/auth/device-code, sign in, and enter this code: **ABCD-2345**

The code goes into the Dex page they land on, never back to you. Codes use a
confusable-free alphabet (no `0`, `O`, `1`, `I`, `L`) and are case-insensitive; the
hyphen is cosmetic.

### 4c. Poll

```http
POST https://mcp.getdex.com/device/token
Content-Type: application/json

{ "device_code": "<hex>" }
```

Honor `interval` (5s). Responses:

| Response                                | Meaning                                                      |
| --------------------------------------- | ------------------------------------------------------------ |
| `200 {"error":"authorization_pending"}` | Not approved yet. Wait `interval` and poll again.            |
| `429 {"error":"slow_down"}`             | You polled faster than `interval`. Back off.                 |
| `400 {"error":"expired_token"}`         | The 10-minute window closed. Restart at 4a.                  |
| `400 {"error":"invalid_request"}`       | No `device_code` in the body.                                |
| `200 {"api_key":"dex_..."}`             | Approved. This is your credential — it is returned **once**. |

Approval requires a Dex account on the **Professional** plan; the approval page
tells the user if an upgrade is needed.

---

## Step 5 — The REST API is a separate resource

`https://api.prod.getdex.com` is a different resource server with a different
credential, and this is the single most important thing in this document to get
right.

**The REST API does not accept MCP access tokens.** The JWTs minted at
`https://mcp.getdex.com/token` are for `https://mcp.getdex.com/mcp` and nothing
else. Presenting one to the REST API gets you a `401`.

**The REST API accepts `dex_` API keys** (and, for first-party surfaces, Firebase
ID tokens):

```http
GET https://api.prod.getdex.com/v1/contacts
Authorization: Bearer dex_<key>
```

Two ways to get one:

1. **Self-serve, in the app.** A signed-in user generates a key in
   **Settings → Integrations** (Professional plan required). This is the path for a
   long-lived credential.
2. **Device flow.** [Step 4](#step-4--device-flow-headless-clients) mints one for a
   headless client.

A key acts as its owner across the endpoints marked public in the spec, in one
place: the owner's Personal Dex, or the one workspace it was created for. It is
limited to reading if it was created Read-only. Treat a Read & write key as a full
credential for that place. Users revoke keys from the same
settings screen.

API surface, in machine-readable form:

- OpenAPI 3.0 description: <https://api.prod.getdex.com/docs/json>
  (also reachable as <https://getdex.com/openapi.json>, which redirects there)
- Documentation: <https://getdex.com/docs/api-reference/authentication>
- RFC 9727 API catalog: <https://getdex.com/.well-known/api-catalog>

### Why there is no PRM on the API host

RFC 9728 Protected Resource Metadata names the authorization server whose tokens a
resource accepts. Dex deliberately does **not** publish PRM on
`api.prod.getdex.com`: the only authorization server Dex runs is the MCP one, and
the REST API does not consume its tokens. A PRM there would point you at
`https://mcp.getdex.com/`, you would complete a full OAuth flow, and every
subsequent request would still `401`.

Publishing that document would score better on a checklist and cost you a wasted
round trip. This section is the honest substitute: the REST API's credential is a
`dex_` API key, obtained as described above.

---

## Step 6 — Use the credential

Against the MCP server:

```http
POST https://mcp.getdex.com/mcp
Authorization: Bearer <access_token or dex_key>
Content-Type: application/json
Accept: application/json, text/event-stream
```

The MCP endpoint is Streamable HTTP and accepts **either** an OAuth access token
**or** a `dex_` API key in the same header — a key beginning `dex_` is routed to
API-key validation, anything else to OAuth verification.

Against the REST API: `Authorization: Bearer dex_<key>`, as in Step 5.

---

## Errors

| Status / code            | Where                  | What to do                                                                                    |
| ------------------------ | ---------------------- | --------------------------------------------------------------------------------------------- |
| `401 invalid_token`      | `/mcp`                 | Missing, malformed or expired credential. Refresh (3d) or restart at Step 1.                  |
| `403 insufficient_scope` | `/mcp`                 | Valid credential, but the account lacks a Professional subscription. A re-auth will not help. |
| `invalid_client`         | `/authorize`, `/token` | The registration lapsed. Re-register (3a) and retry once.                                     |
| `invalid_grant`          | `/token`               | Code or refresh token expired, consumed or replayed. Restart at 3b.                           |
| `unsupported_grant_type` | `/token`               | Only `authorization_code` and `refresh_token` are accepted.                                   |
| `authorization_pending`  | `/device/token`        | Keep polling at `interval`.                                                                   |
| `slow_down`              | `/device/token`        | Add at least 5s to your interval.                                                             |
| `expired_token`          | `/device/token`        | Restart at 4a.                                                                                |
| `429`                    | REST API               | Respect `Retry-After` (seconds). See the `RateLimit-*` headers.                               |

REST API errors are typed. Every non-2xx returns
`{ "error": { "code": "...", "message": "...", "hint"?: "...", "details"?: {...} } }`.
Pattern-match on `error.code`, never on `error.message`.

Retry policy: 5xx → exponential backoff on the same request. 4xx → do not replay
the same payload; act on the table above.

---

## Revocation

- **Access and refresh tokens** — `POST https://mcp.getdex.com/revoke` with
  `token=<token>` (form-encoded, `client_secret_post` auth), per RFC 7009. Idempotent.
- **Client registrations** — expire after 90 days of inactivity. Re-register (3a).
- **API keys** — revoked by the user in **Settings → Integrations**. There is no
  agent-callable revocation endpoint for them; a revoked key starts returning `401`
  within the validation cache window.
- **A user disconnecting your client** in Dex kills its tokens. On a `401` for a
  previously-working credential, try one refresh (3d); if that returns
  `invalid_grant`, the grant is gone — restart at Step 3 and ask the user to
  re-consent.

---

## What Dex does not implement

Stated explicitly so you do not probe for these and read the 404s as an outage.
From the auth.md profile, Dex serves **no**:

- `identity_endpoint` — there is no `POST /agent/identity`. Registration is RFC 7591
  DCR at `register_uri`, and that is the whole of it.
- `claim_endpoint` — no claim ceremony. The device flow in Step 4 covers the same
  ground with RFC 8628 vocabulary.
- `events_endpoint` — Dex ingests no RFC 8935 security event tokens.
- `identity_assertion` support — Dex verifies **no** third-party identity
  assertions. There is no ID-JAG trust list, no issuer allowlist, and no
  `urn:ietf:params:oauth:grant-type:jwt-bearer` grant. If you hold an ID-JAG for a
  Dex user, there is nothing here that will accept it; use Step 3 instead.
- `service_auth` — Dex mints nothing from a bare `login_hint`.

`agent_auth.identity_types_supported` lists `anonymous` only, for exactly this
reason.

---

## More

- MCP server manifest: <https://getdex.com/.well-known/mcp.json>
- MCP server card: <https://getdex.com/.well-known/mcp/server-card.json>
- Site overview for agents: <https://getdex.com/llms.txt>
- MCP server docs: <https://getdex.com/docs/ai/mcp-server>
- Contact: contact@getdex.com
