---
title: "oxom — Agent Authentication"
description: "How an AI agent obtains and uses credentials for an oxom workspace: OAuth 2.0 authorization code flow with PKCE against app.oxom.de, for the MCP resource server at mcp.oxom.de."
canonical: https://oxom.de/auth.md
last-updated: 2026-08-24
---

# oxom — Agent Authentication

> How an AI agent obtains credentials to act on an oxom workspace. oxom runs a
> standard OAuth 2.0 authorization code flow with PKCE. There is no API-key
> path, and no anonymous access to workspace data.

The short version: the **authorization server** is `https://app.oxom.de`, the
**resource server** is `https://mcp.oxom.de/mcp`, and the client id for agent
clients is `oxom-claude` (a public PKCE client — no secret).

## Discover

Start from the resource server. An unauthenticated request to the MCP endpoint
returns `401` with a spec-shaped `WWW-Authenticate` header naming the
metadata document:

```http
POST https://mcp.oxom.de/mcp
```

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
  error_description="No authorization provided",
  resource_metadata="https://mcp.oxom.de/.well-known/oauth-protected-resource"
```

Fetch that document for RFC 9728 protected-resource metadata, which names the
authorization server:

- Protected resource metadata (RFC 9728): `https://mcp.oxom.de/.well-known/oauth-protected-resource`
- Authorization server metadata (RFC 8414): `https://app.oxom.de/.well-known/oauth-authorization-server`

The `agent_auth` block in the authorization server metadata points back at
this document. `https://oxom.de/.well-known/oauth-authorization-server` and
`https://oxom.de/.well-known/oauth-protected-resource` both `307` to the
authoritative documents above, so probing the apex domain also works.

## Pick a method

oxom supports exactly one method. There is no anonymous tier and no
`identity_assertion` / `id-jag` token exchange — an agent always acts as a
human workspace member who granted consent.

| Method | Supported | Notes |
| --- | --- | --- |
| `authorization_code` + PKCE (`S256`) | **Yes** | The only supported path. |
| `refresh_token` | **Yes** | Issued alongside every access token. |
| `anonymous` | No | Workspace data is never readable without consent. |
| `identity_assertion` (`urn:ietf:params:oauth:token-type:id-jag`) | No | Not implemented. |
| API keys / client credentials | No | No machine-only credential exists. |

## Register

Clients are **pre-registered**. RFC 7591 dynamic client registration is not
implemented, so there is no `register_uri` to POST to — an agent that tries
one will get a 404, and that is expected rather than a misconfiguration.

Use the public client id:

```
client_id: oxom-claude
client_secret: (none — public PKCE client)
```

Redirect URIs are pinned exactly. Two are registered:

- `https://claude.ai/api/mcp/auth_callback` — claude.ai, Claude Desktop, mobile.
- An RFC 8252 loopback redirect (`http://localhost` or `http://127.0.0.1`,
  any port, path `/callback`) — for CLI and local agent clients.

If you need a different redirect URI registered, email support@oxom.de — this
is a manual step, deliberately.

## Claim

Send the user through the consent screen. The user picks **one workspace** at
consent time, and the resulting token is bound to that workspace for its whole
lifetime; there is no cross-workspace token.

```http
GET https://app.oxom.de/oauth/authorize
  ?response_type=code
  &client_id=oxom-claude
  &redirect_uri=<your registered redirect URI>
  &code_challenge=<S256 challenge>
  &code_challenge_method=S256
  &scope=links:read+links:write
  &state=<opaque CSRF value>
```

The redirect back carries `code`, `state`, and — per RFC 9207 — an `iss`
parameter identifying the authorization server. Verify `state` and `iss`
before exchanging.

Exchange the code:

```http
POST https://app.oxom.de/api/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=<same redirect URI>
&client_id=oxom-claude
&code_verifier=<PKCE verifier>
```

### Scopes

Request the narrowest set that covers the job.

| Scope | Grants |
| --- | --- |
| `links:read` | Read short links and their click counts. |
| `links:write` | Create and update short links. |
| `docs:read` | Search oxom Docs metadata in the workspace. |
| `docs:write` | Create oxom Docs. |
| `drive:read` | List and search files in the workspace Drive. |
| `drive:write` | Create signed upload links for the workspace Drive. |
| `workspace:read` | Read workspace profile, membership, and Team Time availability. |

## Use the credential

Send the access token as a bearer token to the MCP endpoint over Streamable
HTTP:

```http
POST https://mcp.oxom.de/mcp
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: application/json, text/event-stream
```

The token identifies the workspace, so no workspace or tenant parameter is ever
needed on individual calls. Tool descriptions and input schemas come from
`tools/list`; a preview of the tool set is available without authenticating at
`https://oxom.de/.well-known/mcp/server-card.json`.

## Errors

Failures are JSON, never HTML.

| Status | `error` | What to do |
| --- | --- | --- |
| `401` | `invalid_token` | Token missing, malformed, or expired. Refresh, then retry once. The `WWW-Authenticate` header names the metadata URL. |
| `403` | `insufficient_scope` | The token is valid but lacks a required scope. Re-run the authorization step requesting that scope. Do not retry as-is. |
| `403` | `workspace_forbidden` | The consenting user is no longer a member of the bound workspace. Re-authorize; a refresh will not fix it. |
| `404` | — | The workspace or object does not exist, or the token's workspace cannot see it. |
| `429` | `rate_limited` | Back off using `Retry-After`. |

Inside an established MCP session, tool-level failures come back as JSON-RPC
errors with a numeric `code` and a human-readable `message` — not as
transport-level HTTP errors.

## Revocation

- **The user** can revoke a connection at any time from
  `https://app.oxom.de` → workspace settings → connected apps. Revocation is
  immediate and invalidates the refresh token as well as the access token.
- **Removing the user** from the workspace invalidates their tokens for it.
- **An agent** should discard stored tokens on a `403 workspace_forbidden`
  and re-run the flow from *Claim* rather than retrying with a refresh token.

There is no programmatic `revocation_uri` (RFC 7009) yet. Revocation is
user-driven through the workspace UI; an agent that needs to drop access simply
deletes its stored tokens.

## Contact

Questions about agent access, or a redirect URI to register: support@oxom.de
