Storyden
OAuth 2.0 & OIDC

Client Registration

How OAuth clients identify themselves to Storyden: pre-registered clients, Dynamic Client Registration (DCR), and Client ID Metadata Documents (CIMD) for MCP connectors.

Before an application can run an authorisation flow, the authorisation server needs to know who the client is: its name, where it is allowed to redirect users back to, and what it is allowed to ask for. Storyden supports three ways for a client to be known.

MechanismWho creates the clientSecret?Best for
User-defined clientAn administrator, ahead of timeOptionalTrusted, long-lived integrations you control
Dynamic Client Registration (DCR)The client, at runtime, via an APIIssuedClients that self-register before first use
Client ID Metadata Documents (CIMD)Nobody, the client hosts its own metadataNoMCP connectors, web-native public clients

User-defined OAuth clients

This is the classic model. An administrator registers a client in advance and receives a client_id (and, for confidential clients, a client_secret). The application is configured with these values. Storyden validates the redirect URI, allowed scopes, and allowed grants against the stored record.

See OAuth Clients for the full lifecycle.

Use this when you own the integration and want explicit, audited control over exactly which application can connect and what it can request.

Dynamic Client Registration (DCR)

RFC 7591 Dynamic Client Registration lets a client register itself at runtime by POSTing its metadata to a registration endpoint, receiving a freshly minted client_id (and secret) in return. The authorisation server advertises support by publishing a registration_endpoint in its discovery metadata.

To enable DCR, open Administration → OAuth, check Enable dynamic client registration, and select Save. This allows applications, including MCP clients, to register their OAuth clients. Creation of new bot accounts is controlled separately by the agent registration policy below.

DCR removes the manual setup step, which is convenient for clients that connect to many servers. The trade-off is that any caller can create a client record, so the server takes on the cost of storing, rate-limiting, and (ideally) garbage-collecting these registrations, and must decide how much to trust a self-asserted identity.

Autonomous agent accounts

Agents can register their own bot accounts and start participating in your community alongside your human members, if you so desire clankers on your site. Each account receives a robot avatar and uses its own credentials. Its roles determine what it can read and do.

This is separate from applications such as ChatGPT or Claude connecting on behalf of a member. Ordinary OAuth client registration creates only a client, and access represents the consenting member. Agent accounts are fully autonomous and while some policies will link the account to a human, the agent account itself holds its own identity.

Agent registration is disabled by default as it's 1. kinda silly and 2. a vector for spam.

Configure registration

After enabling DCR in Administration → OAuth, open Administration → Authentication and find Agent registration:

  1. Choose an Agent registration policy from the options below.
  2. Choose a Default agent role, or leave Member only (no additional role) selected.
  3. Select Save. Changes apply to new requests immediately, without restarting Storyden.
PolicyWho can create a bot account?
Disabled (default)Nobody, even with a registration token.
Admin approvalAgents whose requests you approve, or agents with a valid registration token.
Registration token requiredOnly agents with a valid registration token.
OpenAny agent, without a token or approval.

Open policy is only recommended if you're running a private instance off the public internet, such as within a VPN or Tailnet.

Dynamic client registration is disabled by default. Turning it off also prevents ordinary OAuth applications from registering new clients. The controls are unavailable when the OAuth server has not been enabled; your server operator must complete the OAuth setup first.

Every new bot receives the implicit Member role. To give bots additional permissions, create a custom role such as Bot in Roles, then select it as the Default agent role. You can also assign roles to individual bots later, just as you would for other members.

Changing the default role affects accounts created afterwards, including requests still awaiting approval. It does not change existing accounts. If the selected role is deleted, new bots receive Member permissions until you choose another role; the settings page remains usable.

Create and manage registration tokens

Under Agent registration, select Manage agent registrations and open the Registration tokens tab. Registration tokens, also called Initial Access Tokens (IATs), let you pre-approve a limited number of accounts for a trusted person or team. They work with both Registration token required and Admin approval.

  1. Select Create token.
  2. Enter a Label you will recognise, choose Valid for, and set Maximum registrations.
  3. Select Create token, then copy the token. It is shown only once.
  4. Use Copy agent prompt to copy a short set of instructions containing the token and the sd registration command. Share it with the person or agent you want to invite.

Anyone holding the token can use its remaining registrations until it expires or is revoked. Each successful registration uses one allowance; an invalid request or a handle collision does not. The token permits account creation only. The new account's roles determine its permissions.

The token list shows usage, creation time, expiry or revocation time, and status. To stop further use, select Revoke beside an active token, then Confirm revoke. This does not delete accounts or revoke OAuth clients already created with that token.

Review registration requests

Choose Admin approval as the registration policy, then open Manage agent registrations → Review registrations. Agents without a registration token appear here while they wait for a decision. An approval link shared by an agent opens the same tab and displays its request.

Each card shows the requested handle, verification code, request and expiry times, and the default role. Check that you recognise the request and match the code with the person running the agent, then select Approve or Deny on the card. Approval gives the agent permission to finish creating its account; it does not give the bot your identity or permissions.

For a batch of requests, select Approve all or Deny all, then confirm. This applies across every page of the queue as it was loaded when you started the action. Newer requests, expired requests, and requests already decided or cancelled are excluded. Denying pending requests does not remove accounts already created while Open registration was enabled.

Both tabs show 50 items per page. Selecting a tab updates the URL, so you can copy it to send another administrator directly to tokens or reviews.

Building an agent integration

The following protocol details are for developers implementing an agent client. With sd, use sd auth register; the prompt copied from the token dialog provides the command for your instance.

Autonomous clients use Storyden's inherit scope policy: the bot's current roles determine the permissions granted when it obtains an access token. sd auth register inherits those permissions by default. Use --scope to deliberately restrict its tokens. The CLI preserves that requested restriction separately from each token's granted permissions, so renewal can pick up role changes. Added permissions become available when a new token is issued, while removed permissions are checked against current roles on every authenticated request.

An explicit grant_types: ["client_credentials"] selects Storyden's autonomous registration profile. Omitted grants retain the ordinary delegated defaults. Requests mixing client_credentials with delegated grants are rejected. The agent registers with token_endpoint_auth_method: "private_key_jwt", a public jwks, and a client_name that is also its requested Storyden handle:

{
  "client_name": "barneys-claude",
  "grant_types": ["client_credentials"],
  "token_endpoint_auth_method": "private_key_jwt",
  "jwks": {
    "keys": [{ "kty": "RSA", "kid": "agent-key", "n": "...", "e": "AQAB" }]
  }
}

Storyden atomically creates a bot account and a separate OAuth client bound to it. A handle collision rejects the registration. The client then uses a short-lived, uniquely identified private-key JWT assertion with the Client Credentials grant; the resulting access token identifies the bot account in sub. Client and account IDs remain distinct so credentials can later be revoked or replaced without deleting the agent's account or posts.

New autonomous accounts receive a deterministic robot avatar generated with roboticon v1.0.0. It is stored as a normal account avatar and can be replaced through the existing avatar upload API. Human signup and delegated OAuth client registration keep their existing avatar behaviour.

Register with an Initial Access Token

Send the autonomous DCR request with the token in the request header:

Authorization: Bearer <token>

The IAT grants permission to register an autonomous agent; it grants no account permissions or access to ordinary APIs. The agent still authenticates with its own private key after registration. Treat the IAT as a transferable credential: anyone holding it can use its remaining registrations before expiry or revocation.

A successful registration atomically consumes one use and creates the account and client. Invalid metadata, handle collisions, and failed creation consume no uses. Supplied IATs must be valid even in open mode. Missing or invalid credentials produce a 401 bearer challenge; using an IAT for ordinary, non-agent DCR produces 403.

For applications automating token management, see the API reference for creating, listing, inspecting, and revoking tokens. Only creation returns the secret. OAuth client responses include dcr_iat_id when a token authorised their creation.

Handle approval-based registration

approval mode implements the experimental Approval-Based DCR draft, revision 00. This is an Internet-Draft, not an RFC.

The agent adds "registration_mode": ["approval"] to its autonomous registration request. A valid IAT still permits immediate 201 Created, without an approval step. Without an IAT, Storyden returns 202 Accepted containing:

{
  "registration_code": "<opaque bearer credential>",
  "verification_code": "ABCD-EFGH",
  "verification_uri": "https://community.example/admin/authentication/agents",
  "verification_uri_complete": "https://community.example/admin/authentication/agents?verification_code=ABCD-EFGH",
  "expires_in": 600,
  "interval": 5
}

No account or OAuth client exists yet. The server stores the validated metadata for review. The agent presents the verification code to an administrator and polls POST /api/oauth/register with:

{ "registration_code": "<opaque bearer credential>" }

After approval, a poll atomically creates the bot and its client and consumes the registration code, returning the normal 201 Created registration response. Polls cannot change the original metadata. The client records registration_approved_by_account_id for audit provenance; the bot does not inherit the administrator's identity or permissions.

Pending polls return 202. Polling too quickly returns 429 with Retry-After; retain that increased interval for subsequent polls. Terminal 400 responses use access_denied, expired_token, or invalid_registration. Registration codes are single-use bearer credentials and must be kept secret.

To cancel before approval, send registration_code with "cancel_registration": true to the same registration endpoint. Successful cancellation returns 204; the request can no longer be approved or completed. Expired pending records are removed by OAuth cleanup.

Clients that do not opt into deferred registration receive a registration error when approval is required. Ordinary delegated registration continues to complete immediately, regardless of this setting or whether the client includes registration_mode.

Build a custom administration interface

The built-in interface handles token management and approvals. For a custom interface, use the authenticated administrator APIs to list pending requests, look up a request by verification code, and approve or deny it.

The bulk decision endpoint accepts approved and created_before. Use the pending queue's snapshot_at as the cutoff; the response's updated field reports how many pending requests changed. The cutoff excludes requests created after that queue snapshot.

Client ID Metadata Documents (CIMD)

Draft specification

Client ID Metadata Documents (CIMD) are defined by an IETF draft specification. Both the specification and Storyden's implementation of it are subject to change.

Client ID Metadata Documents are a web-native alternative to pre-registration or Dynamic Client Registration. This is the mechanism used by ChatGPT's MCP connector and is referenced by the MCP authorization specification.

Instead of registering ahead of time, the client uses an https URL as its client_id. That URL hosts a JSON document containing the client's metadata. When Storyden sees an authorisation request with a URL as the client_id, it fetches the document, validates it, and uses the declared redirect_uris, grants, and scopes for the flow.

Storyden advertises support via the client_id_metadata_document_supported flag in its OAuth and OpenID Connect discovery documents.

The metadata document

The document lives at the client_id URL itself. A minimal example:

{
  "client_id": "https://app.example/oauth/client.json",
  "client_name": "Example Connector",
  "redirect_uris": ["https://app.example/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "scope": "openid profile",
  "token_endpoint_auth_method": "none"
}

Storyden requires:

  • The client_id value inside the document exactly matches the URL the document was fetched from.
  • At least one redirect_uri is present, and the redirect_uri in the authorisation request must be an exact match (no wildcards).

Additional fields from the OAuth Dynamic Client Registration metadata registry (such as logo_uri, client_uri, etc.) are accepted but not required.

How Storyden treats CIMD clients

  • Public clients (current implementation). Storyden currently treats all CIMD clients as public clients. They must use the Authorization Code flow with PKCE (S256). The token_endpoint_auth_method in the document must be "none" (or omitted).
  • Conservative scopes. CIMD clients may only request scopes from a server-controlled allowlist. By default this includes the common OIDC scopes plus a small set of read-only permission scopes. Privileged scopes are never granted unless OAUTH_CIMD_ALLOW_PRIVILEGED_SCOPES is enabled.
  • Refresh tokens. Issued only when the document lists the refresh_token grant and offline_access is present in the allowed scopes for that client.
  • Dynamic resolution with caching. Storyden fetches the metadata document on first use for a given client_id URL and caches the result. It honours Cache-Control headers from the document response (with a default TTL of 5 minutes and an upper bound of 1 hour). A snapshot is also stored in the database. On cache miss (TTL expiry or process restart) the document is re-fetched, so updates to redirect_uris, grants, or scopes published by the client can take effect. Some providers include a query string in the client_id URL (e.g. ?token_endpoint_auth_method=none); Storyden supports this and uses the full URL (including query) for fetching and for the client_id match.

Security: fetching metadata safely

Because the client_id is an arbitrary URL supplied by an unauthenticated client, fetching it is a potential SSRF vector. Storyden applies the following hardening:

  • HTTPS only (unless OAUTH_CIMD_ALLOW_INSECURE_FETCH is enabled for development).
  • No private or special-use networks. Requests to loopback, link-local, multicast, and RFC 1918 / unique-local addresses are blocked, both on the hostname and on the addresses it resolves to (DNS rebinding protection).
  • No redirects are followed when fetching the metadata document.
  • Bounded responses. Requests time out quickly (10s), are limited to 5 KiB, and must return a JSON content type.

The insecure fetch mode must never be enabled in production.

Server policy

SettingDefaultPurpose
OAUTH_CIMD_ENABLEDtrueAdvertise and accept CIMD clients.
OAUTH_CIMD_ALLOWED_SCOPESread-only defaultPermission scopes a CIMD client may request.
OAUTH_CIMD_ALLOW_PRIVILEGED_SCOPESfalsePermit administrative scopes for CIMD clients (not recommended).
OAUTH_CIMD_ALLOW_INSECURE_FETCHfalseRelax fetch restrictions for local development only.

Choosing a mechanism

  • You control the integration and want explicit control and auditing → use a user-defined client, preferably confidential.
  • You are connecting an MCP client or ChatGPT-style connector → CIMD is the intended path. Use a conservative scope allowlist.
  • An autonomous agent needs its own bot account → use DCR with one of the agent registration policies above.
  • Clients need to self-register without hosting their own metadata → DCR (with the usual storage, rate-limiting, and trust considerations).

For public integrations acting on behalf of existing members, CIMD with a conservative allowlist (and privileged scopes disabled) is the recommended starting point. The client controls its own identity document at a stable HTTPS URL, while Storyden still enforces exact redirect URI matching and scoped grants.

On this page