CLI OAuth and Dynamic Client Registration
How the MarkDocket CLI performs browser-based OAuth 2.1 login using RFC 7591 dynamic client registration and PKCE, with no embedded client_id in the distributed package.
The markdocket login command opens a browser-based OAuth 2.1 authorization flow. Rather than shipping a fixed client_id inside the CLI package, the CLI dynamically registers a fresh OAuth client with the MarkDocket authorization server on every login attempt, following RFC 7591 (OAuth 2.0 Dynamic Client Registration). Combined with PKCE (Proof Key for Code Exchange), this design means the distributed CLI binary contains no embedded credentials that could be extracted or abused.
Why no embedded client_id?
Traditional CLI OAuth applications embed a static client_id (and sometimes a client_secret) in the distributed package. Anyone who installs the package can extract that identifier and impersonate the client. Because the MarkDocket CLI is a public npm package (@markdocket/cli) that runs on arbitrary end-user machines, embedding a static identity would provide no meaningful security boundary.
Instead, the CLI registers a brand-new client for each login session. That client exists only for the duration of the login flow and is scoped exactly to the loopback redirect URI chosen for that session. Nothing in the published package identifies a long-lived OAuth client.
Login flow step by step
1. Discover the authorization server metadata
The CLI fetches the authorization server's metadata document from /.well-known/oauth-authorization-server. This document contains the registration_endpoint, authorization_endpoint, and token_endpoint URLs the rest of the flow depends on. Using discovery rather than hardcoded paths means the CLI automatically adapts to any environment the server is deployed to.
2. Bind a loopback redirect URI to a random port
The CLI opens a temporary HTTP listener on localhost at a randomly chosen port. The redirect URI registered with the server is the exact loopback address for that port — for example, http://127.0.0.1:52341/callback. This is intentional: if the port were fixed, a malicious process on the same machine could pre-bind that port and intercept the authorization code. Using a random ephemeral port limits this attack surface.
3. Dynamically register a new public OAuth client (RFC 7591)
The CLI sends a POST to the registration_endpoint with a client metadata document that:
- Declares only the single loopback redirect URI chosen in step 2
- Requests
token_endpoint_auth_method: none(a public client — no secret) - Declares the required grant type and scopes for the session
The server responds with a freshly issued client_id that is valid for this registration. No client_secret is issued or stored. This is the RFC 7591 dynamic registration protocol.
4. Generate a PKCE challenge
The CLI generates:
- A code verifier from 48 bytes of cryptographically random data, Base64url-encoded
- A code challenge by hashing the verifier with SHA-256 (
S256method) and Base64url-encoding the result - A state parameter from 24 bytes of cryptographically random data
The verifier is held in memory only; it is never written to disk or transmitted to the server at this stage.
5. Open the browser to the authorization URL
The CLI constructs the authorization URL with:
- The dynamically registered
client_id - The loopback
redirect_uri code_challengeandcode_challenge_method=S256- The random
statevalue
The URL is opened in the user's default browser. The user sees the MarkDocket login page (or is already signed in) and approves the request on the OAuth consent page.
6. Receive the authorization code on the loopback listener
After the user approves, the authorization server redirects to the loopback URI. The CLI's local HTTP listener receives the request, extracts the code and state parameters, and immediately shuts down the listener.
The CLI validates that the returned state matches the value generated in step 4, protecting against cross-site request forgery on the loopback callback.
7. Exchange the code for tokens using the PKCE verifier
The CLI sends the authorization code to the token_endpoint along with:
- The
code_verifier(the raw random bytes from step 4) - The
redirect_uriandclient_idfrom the registration
The server hashes the verifier with SHA-256 and confirms it matches the code_challenge sent in step 5. This proves the entity completing the token exchange is the same entity that initiated the authorization request, even though no client secret was used.
The server responds with an OAuth 2.1 access token (and optionally a refresh token). The CLI stores these credentials in the user's local credential store.
8. Route tool calls through the hosted MCP endpoint
Once authenticated via browser OAuth, the CLI routes all tool invocations through the hosted MCP endpoint (/mcp) rather than calling the API directly. OAuth 2.1 access tokens issued through this flow are resource-bound to the MCP endpoint and cannot be used against internal API routes. This is an intentional security boundary: the MCP endpoint enforces the same tool catalog and scope restrictions as the consent page disclosed, and the access token has no broader API surface.
This differs from personal-API-key sessions, which call API routes directly. The CLI selects the appropriate execution transport based on the credential type stored at login time.
Security properties at a glance
| Property | How it is achieved |
|---|---|
| No embedded credentials in the package | RFC 7591 dynamic registration issues a fresh client_id per session |
| Code interception protection | PKCE S256 — the verifier never leaves the CLI process until the token exchange |
| Redirect hijacking protection | Random ephemeral loopback port; URI bound to the registered client |
| CSRF protection on callback | 24-byte random state parameter validated before code use |
| Narrow token scope | Access token is resource-bound to the MCP endpoint only |
| No client secret | Public client (token_endpoint_auth_method: none); PKCE replaces the secret |
Account creation and re-login
If you do not yet have a MarkDocket account, create one at app.markdocket.com first, then run markdocket login again. The CLI does not automatically resume a pending authorization flow after account creation — you need to start a new markdocket login invocation.
Logging out
Running markdocket logout removes the locally stored credentials. The access token is not actively revoked at the server at logout time; it will expire according to its normal lifetime. If you need to revoke access immediately, you can manage authorized applications from your MarkDocket account settings.
No client_id to configure
Comparison with personal API key authentication
The CLI supports two authentication modes:
Browser OAuth (markdocket login) — follows the flow described on this page. Best for interactive use. Token scope is limited to the MCP endpoint. Suitable for all read and write operations exposed through the tool catalog.
Personal API key (markdocket login --api-key) — you supply an API key generated in your account settings. The key is stored locally and sent as a bearer token. API key sessions call the API directly rather than routing through the MCP endpoint. API keys do not expire automatically and should be treated as long-lived secrets.
For automated or CI environments where no browser is available, use a personal API key.
Usage Metering
Explains how MarkDocket records usage events through a transactional outbox, the retry and dead-letter behavior of the drain loop, and the distinction between product usage metering and direct COGS metering.
API Key and MCP Route Boundary
How personal API keys and OAuth 2.1 access tokens are restricted to cataloged tool routes via HMAC provenance signatures, what routes external tokens cannot reach, and how the tool catalog enforces this boundary across the CLI, stdio MCP server, and hosted MCP endpoint.