> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xquik.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth 2.1 Authorization

> OAuth 2.1, PKCE, CIMD, and DCR for Xquik MCP authentication

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Xquik supports [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1) Authorization Code with S256 PKCE for remote MCP authentication. Modern clients discover the flow automatically from `https://xquik.com/mcp`, open Xquik login and consent in a browser, then store and refresh Bearer tokens.

<Info>
  Prefer OAuth for Claude, ChatGPT, Codex, Cursor, VS Code, Windsurf,
  OpenCode, Gemini CLI, and other compatible remote MCP clients. Xquik also
  accepts [API keys](/api-reference/authentication) when a client cannot run
  OAuth but can store headers securely.
</Info>

Xquik supports both MCP client registration paths:

* **Client ID Metadata Documents (CIMD)**: Recommended for modern clients. The client's HTTPS metadata URL becomes its `client_id`. No registration request or client secret is required.
* **Dynamic Client Registration (DCR)**: Compatibility fallback for clients that do not publish CIMD. The client registers itself once at `/api/oauth/register`.

Normal users do not create either configuration manually. Enter
`https://xquik.com/mcp` in the client and follow its authentication prompt.

## How it works

OAuth 2.1 Authorization Code with PKCE follows this sequence:

```text theme={null}
MCP Client                         Xquik
    │                                 │
    │  1. Discover auth metadata      │
    │────────────────────────────────▶│
    │◀────────────────────────────────│
    │  2. Use CIMD or register by DCR │
    │                                 │
    │  3. Generate code_verifier      │
    │     + code_challenge            │
    │                                 │
    │  4. Redirect to /authorize      │
    │────────────────────────────────▶│
    │                                 │  User logs in
    │                                 │  + approves access
    │  5. Redirect back with code     │
    │◀────────────────────────────────│
    │                                 │
    │  6. Exchange code + verifier    │
    │────────────────────────────────▶│
    │◀────────────────────────────────│
    │    access_token + refresh_token │
    │                                 │
    │  7. Call MCP with Bearer token  │
    │────────────────────────────────▶│
```

## Discovery

Xquik publishes standard OAuth discovery documents so MCP clients can auto-configure endpoints.

### Authorization server metadata

```bash theme={null}
curl https://xquik.com/.well-known/oauth-authorization-server
```

```json Response theme={null}
{
  "issuer": "https://xquik.com",
  "authorization_endpoint": "https://xquik.com/api/oauth/authorize",
  "token_endpoint": "https://xquik.com/api/oauth/token",
  "registration_endpoint": "https://xquik.com/api/oauth/register",
  "scopes_supported": ["mcp:tools"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "revocation_endpoint": "https://xquik.com/api/oauth/revoke",
  "authorization_response_iss_parameter_supported": true,
  "response_modes_supported": ["query"],
  "client_id_metadata_document_supported": true,
  "agent_auth": {
    "register_uri": "https://xquik.com/api/oauth/register",
    "claim_uri": "https://xquik.com/api/oauth/authorize",
    "revocation_uri": "https://xquik.com/api/oauth/revoke",
    "identity_types_supported": ["anonymous", "oauth_client"],
    "credential_types_supported": ["oauth_access_token"],
    "scopes_supported": ["mcp:tools"],
    "skill": "https://xquik.com/auth.md",
    "anonymous": {
      "claim_uri": "https://xquik.com/api/oauth/authorize",
      "credential_types_supported": ["oauth_access_token"],
      "scopes_supported": ["mcp:tools"]
    },
    "oauth_client": {
      "registration_endpoint": "https://xquik.com/api/oauth/register",
      "response_types_supported": ["code"],
      "grant_types_supported": ["authorization_code", "refresh_token"],
      "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
      "credential_types_supported": ["oauth_access_token"],
      "scopes_supported": ["mcp:tools"]
    }
  }
}
```

### Protected resource metadata

```bash theme={null}
curl https://xquik.com/.well-known/oauth-protected-resource/mcp
```

```json Response theme={null}
{
  "resource": "https://xquik.com/mcp",
  "resource_name": "Xquik MCP Server",
  "authorization_servers": ["https://xquik.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["mcp:tools"]
}
```

The MCP endpoint also returns this metadata URL in its unauthenticated
`WWW-Authenticate` challenge:

```text theme={null}
Bearer realm="OAuth", resource_metadata="https://xquik.com/.well-known/oauth-protected-resource/mcp", scope="mcp:tools"
```

## Client registration choices

### Client ID Metadata Document

Publish a JSON document at a stable HTTPS URL and use that exact URL as the
`client_id`:

```json theme={null}
{
  "client_id": "https://client.example/oauth/client.json",
  "client_name": "Example MCP Client",
  "redirect_uris": ["https://client.example/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

The document must use `application/json`, remain at or below 5 KiB, repeat the
exact client ID URL, and list every allowed redirect URI. Xquik fetches CIMD
without redirects and accepts public clients with token authentication method
`none`.

### Dynamic Client Registration

Clients without CIMD may register at `POST /api/oauth/register`. DCR supports
public clients with `none` and confidential clients with
`client_secret_post`. The manual flow below uses a DCR-issued UUID so each step
can show a concrete `client_id`.

## Manual implementation

<Steps>
  <Step title="Register a DCR client">
    Skip this step when the client uses CIMD. For DCR, register once to get a
    UUID `client_id`.

    ```bash theme={null}
    curl -X POST https://xquik.com/api/oauth/register \
      -H "Content-Type: application/json" \
      -d '{
        "client_name": "My MCP Client",
        "redirect_uris": ["https://myapp.example.com/callback"]
      }'
    ```

    ```json Response theme={null}
    {
      "client_id": "550e8400-e29b-41d4-a716-446655440000",
      "client_name": "My MCP Client",
      "redirect_uris": ["https://myapp.example.com/callback"],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "token_endpoint_auth_method": "none"
    }
    ```

    **Redirect URI requirements:**

    * Production: HTTPS only
    * Development: `http://localhost` and `http://127.0.0.1` are allowed
    * Exact match required. No wildcards or subpath matching

    **Client types:**

    * **Public** (`token_endpoint_auth_method: "none"`): Default. No client secret. Used by browser apps and MCP clients.
    * **Confidential** (`token_endpoint_auth_method: "client_secret_post"`): Returns a `client_secret` in the registration response. Used by server-side apps.

    <Warning>
      If you register a confidential client, the `client_secret` is returned **once** in the registration response. Store it securely.
    </Warning>
  </Step>

  <Step title="Generate PKCE parameters">
    Generate a cryptographically random `code_verifier` and derive the `code_challenge` from it.

    <CodeGroup>
      ```javascript Node.js theme={null}
      import { randomBytes, createHash } from "node:crypto";

      const codeVerifier = randomBytes(32).toString("hex");
      const codeChallenge = createHash("sha256")
        .update(codeVerifier)
        .digest("base64url");
      ```

      ```python Python theme={null}
      import base64
      import hashlib
      import secrets

      code_verifier = secrets.token_hex(32)
      code_challenge = base64.urlsafe_b64encode(
          hashlib.sha256(code_verifier.encode()).digest()
      ).rstrip(b"=").decode()
      ```

      ```go Go theme={null}
      package main

      import (
      	"crypto/rand"
      	"crypto/sha256"
      	"encoding/base64"
      	"encoding/hex"
      )

      func generatePKCE() (string, string) {
      	b := make([]byte, 32)
      	rand.Read(b)
      	codeVerifier := hex.EncodeToString(b)

      	hash := sha256.Sum256([]byte(codeVerifier))
      	codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])

      	return codeVerifier, codeChallenge
      }
      ```
    </CodeGroup>

    <Warning>
      The `code_verifier` must have sufficient entropy. Use at least 32 cryptographically random bytes (64 hex characters). Store the verifier securely on the client. You need it for the token exchange in step 5.
    </Warning>
  </Step>

  <Step title="Redirect to authorization">
    Redirect the user to the Xquik authorization endpoint with the required query parameters.

    ```text theme={null}
    GET https://xquik.com/api/oauth/authorize
      ?response_type=code
      &client_id=550e8400-e29b-41d4-a716-446655440000
      &redirect_uri=https://myapp.example.com/callback
      &code_challenge=a1b2c3d4e5f6...
      &code_challenge_method=S256
      &scope=mcp:tools
      &state=random_csrf_token
      &resource=https://xquik.com/mcp
    ```

    **Required parameters:**

    | Parameter               | Value                                                  |
    | ----------------------- | ------------------------------------------------------ |
    | `response_type`         | `code`                                                 |
    | `client_id`             | UUID from client registration                          |
    | `redirect_uri`          | Must match a registered URI exactly                    |
    | `code_challenge`        | Base64url-encoded SHA256 digest of the `code_verifier` |
    | `code_challenge_method` | `S256`                                                 |
    | `resource`              | `https://xquik.com/mcp`                                |

    **Optional parameters:**

    | Parameter | Default     | Description                      |
    | --------- | ----------- | -------------------------------- |
    | `scope`   | `mcp:tools` | Only `mcp:tools` is supported    |
    | `state`   |             | Opaque value for CSRF protection |

    Xquik defaults `resource` to `https://xquik.com/mcp` for compatibility, but
    MCP clients must send it in authorization and token requests.

    The user sees a login page (Google OAuth or email magic link) followed by a consent screen. After approval, Xquik redirects back to your `redirect_uri`.
  </Step>

  <Step title="Receive the authorization code">
    After the user approves, Xquik redirects to your `redirect_uri` with a `code` parameter:

    ```text theme={null}
    https://myapp.example.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token
    ```

    Verify the `state` parameter matches the value you sent in step 3 to prevent CSRF attacks. The authorization code expires in **60 seconds** and is single-use.
  </Step>

  <Step title="Exchange code for tokens">
    Exchange the authorization code and your `code_verifier` for an access token and refresh token.

    ```bash theme={null}
    curl -X POST https://xquik.com/api/oauth/token \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=authorization_code\
    &code=AUTH_CODE_HERE\
    &code_verifier=YOUR_CODE_VERIFIER\
    &client_id=550e8400-e29b-41d4-a716-446655440000\
    &redirect_uri=https://myapp.example.com/callback\
    &resource=https://xquik.com/mcp"
    ```

    ```json Response theme={null}
    {
      "access_token": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
      "token_type": "Bearer",
      "expires_in": 86400,
      "refresh_token": "f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1",
      "scope": "mcp:tools"
    }
    ```
  </Step>

  <Step title="Use the access token">
    Pass the access token as a Bearer token in the `Authorization` header when connecting to the MCP server.

    ```bash theme={null}
    curl https://xquik.com/mcp \
      -H "Authorization: Bearer a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
    ```
  </Step>
</Steps>

## Token lifetimes

| Token              | Lifetime   | Notes                                      |
| ------------------ | ---------- | ------------------------------------------ |
| Access token       | 24 hours   | Use the refresh token to get a new one     |
| Refresh token      | 30 days    | Single-use. Each refresh issues a new pair |
| Authorization code | 60 seconds | Single-use. Exchange immediately           |

## Refresh tokens

Access tokens expire after 24 hours. Use the refresh token to get a new access token without requiring the user to log in again.

```bash theme={null}
curl -X POST https://xquik.com/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token\
&refresh_token=f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1\
&client_id=550e8400-e29b-41d4-a716-446655440000\
&resource=https://xquik.com/mcp"
```

```json Response theme={null}
{
  "access_token": "NEW_ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "NEW_REFRESH_TOKEN",
  "scope": "mcp:tools"
}
```

<Warning>
  Refresh tokens are **single-use**. Each refresh request revokes the old refresh token and returns a new one. Always store the latest refresh token from each response.
</Warning>

## Token revocation

Revoke an access or refresh token when a user disconnects or your application no longer needs access.

```bash theme={null}
curl -X POST https://xquik.com/api/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=ACCESS_OR_REFRESH_TOKEN\
&client_id=550e8400-e29b-41d4-a716-446655440000\
&token_type_hint=access_token"
```

| Parameter         | Required | Description                                                                 |
| ----------------- | -------- | --------------------------------------------------------------------------- |
| `token`           | Yes      | The token to revoke                                                         |
| `client_id`       | Yes      | The client ID that owns the token                                           |
| `token_type_hint` | No       | `access_token` or `refresh_token`. Helps the server locate the token faster |

Returns `200` with an empty body on success. If the token is already revoked or invalid, the server still returns `200` (per RFC 7009).

**Revocation errors:**

| Status | Error             | When                                           |
| ------ | ----------------- | ---------------------------------------------- |
| 400    | `invalid_request` | `token` parameter is empty or missing          |
| 400    | `invalid_request` | `client_id` parameter is empty or missing      |
| 401    | `invalid_client`  | `client_id` does not match a registered client |

## Scopes

| Scope       | Description                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------- |
| `mcp:tools` | Full access to all MCP tools (search tweets, manage monitors, run extractions, run draws, etc.) |

Only `mcp:tools` is supported. No partial scopes or scope combinations are available.

## Client registration

### Request

```
POST /api/oauth/register
Content-Type: application/json
```

| Field                        | Type      | Required | Description                                           |
| ---------------------------- | --------- | -------- | ----------------------------------------------------- |
| `client_name`                | string    | Yes      | Display name shown on the consent screen              |
| `redirect_uris`              | string\[] | Yes      | Allowed redirect URIs (1 or more)                     |
| `token_endpoint_auth_method` | string    | No       | `none` (default) or `client_secret_post`              |
| `grant_types`                | string\[] | No       | Defaults to `["authorization_code", "refresh_token"]` |
| `response_types`             | string\[] | No       | Defaults to `["code"]`                                |

### Response

| Field                        | Type      | Description                                                      |
| ---------------------------- | --------- | ---------------------------------------------------------------- |
| `client_id`                  | string    | UUID. Use this in all subsequent OAuth requests                  |
| `client_name`                | string    | Echoed from request                                              |
| `redirect_uris`              | string\[] | Echoed from request                                              |
| `grant_types`                | string\[] | Resolved grant types                                             |
| `response_types`             | string\[] | Resolved response types                                          |
| `token_endpoint_auth_method` | string    | Resolved auth method                                             |
| `client_secret`              | string    | Only present for confidential clients (`client_secret_post`)     |
| `client_id_issued_at`        | number    | Unix timestamp when the client ID was issued                     |
| `client_secret_expires_at`   | number    | Always `0` (non-expiring). Only present for confidential clients |

## Error responses

All errors follow the standard OAuth 2.0 error format:

```json theme={null}
{
  "error": "error_code",
  "error_description": "Human-readable description."
}
```

### Authorization errors

| Error                       | When                                                                                     |
| --------------------------- | ---------------------------------------------------------------------------------------- |
| `unsupported_response_type` | `response_type` is not `code`                                                            |
| `invalid_request`           | Missing `client_id`, `code_challenge`, unknown `client_id`, or mismatched `redirect_uri` |
| `invalid_scope`             | Scope is not `mcp:tools`                                                                 |
| `invalid_target`            | Resource is not `https://xquik.com/mcp`                                                  |
| `access_denied`             | User denied the authorization request                                                    |

### Token errors

| Error                    | When                                                                                                                              |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`        | Missing `code`, `code_verifier`, `client_id`, or `refresh_token`                                                                  |
| `invalid_grant`          | Code/token is invalid, expired, or already used. Also: `client_id` mismatch, `redirect_uri` mismatch, or PKCE verification failed |
| `unsupported_grant_type` | Grant type is not `authorization_code` or `refresh_token`                                                                         |
| `invalid_target`         | Resource is not `https://xquik.com/mcp`                                                                                           |

### Registration errors

| Error                                                                          | Description                          | When                                                      |
| ------------------------------------------------------------------------------ | ------------------------------------ | --------------------------------------------------------- |
| `Invalid request body`                                                         | -                                    | Request body is not valid JSON or not an object           |
| `client_name is required`                                                      | -                                    | Missing or empty `client_name`                            |
| `invalid_client_metadata`                                                      | `client_name exceeds maximum length` | `client_name` exceeds 256 characters                      |
| `redirect_uris must be a non-empty array of strings`                           | -                                    | Missing, empty, or malformed `redirect_uris`              |
| `invalid_client_metadata`                                                      | `Too many redirect URIs`             | Exceeds the 10 redirect URI limit                         |
| `Invalid redirect URI: {uri}. Must be HTTPS or localhost.`                     | -                                    | URI is not HTTPS or localhost                             |
| `Invalid token_endpoint_auth_method. Must be one of: none, client_secret_post` | -                                    | Value is not `none` or `client_secret_post`               |
| `grant_types must be an array of strings`                                      | -                                    | Malformed `grant_types`                                   |
| `invalid_client_metadata`                                                      | `Unsupported grant type`             | Grant type is not `authorization_code` or `refresh_token` |

## Full example

A complete Node.js implementation of the OAuth 2.1 flow:

```javascript Node.js theme={null}
import { randomBytes, createHash } from "node:crypto";
import http from "node:http";

const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440000";
const REDIRECT_URI = "http://localhost:8080/callback";

// Step 1: Generate PKCE parameters
const codeVerifier = randomBytes(32).toString("hex");
const codeChallenge = createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");
const state = randomBytes(16).toString("hex");

// Step 2: Build the authorization URL
const authUrl = new URL("https://xquik.com/api/oauth/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("scope", "mcp:tools");
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("resource", "https://xquik.com/mcp");

console.log("Open this URL in your browser:");
console.log(authUrl.toString());

// Step 3: Start a local server to receive the callback
const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, "http://localhost:8080");

  if (url.pathname !== "/callback") {
    res.writeHead(404);
    res.end();
    return;
  }

  const code = url.searchParams.get("code");
  const returnedState = url.searchParams.get("state");

  // Verify state to prevent CSRF
  if (returnedState !== state) {
    res.writeHead(400);
    res.end("State mismatch");
    return;
  }

  // Step 4: Exchange the authorization code for tokens
  const tokenResponse = await fetch("https://xquik.com/api/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      code_verifier: codeVerifier,
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      resource: "https://xquik.com/mcp",
    }),
  });

  const tokens = await tokenResponse.json();
  console.log("Access token:", tokens.access_token);
  console.log("Refresh token:", tokens.refresh_token);
  console.log("Expires in:", tokens.expires_in, "seconds");

  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Authorization complete. You can close this tab.");
  server.close();
});

server.listen(8080);
```

## Where to go next

<CardGroup cols={2}>
  <Card title="MCP Server" icon="server" href="/mcp/overview">
    Connect AI agents to Xquik via MCP.
  </Card>

  <Card title="MCP Tools Reference" icon="wrench" href="/mcp/tools">
    All MCP tools with input/output schemas.
  </Card>

  <Card title="API Key Auth" icon="key" href="/api-reference/authentication">
    API key authentication for REST API and MCP.
  </Card>

  <Card title="Quickstart" icon="rocket" href="/quickstart">
    Get your API key and make your first request.
  </Card>
</CardGroup>
