Docs
Security

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 (S256 method) 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_challenge and code_challenge_method=S256
  • The random state value

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_uri and client_id from 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

PropertyHow it is achieved
No embedded credentials in the packageRFC 7591 dynamic registration issues a fresh client_id per session
Code interception protectionPKCE S256 — the verifier never leaves the CLI process until the token exchange
Redirect hijacking protectionRandom ephemeral loopback port; URI bound to the registered client
CSRF protection on callback24-byte random state parameter validated before code use
Narrow token scopeAccess token is resource-bound to the MCP endpoint only
No client secretPublic 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

You do not need to register an OAuth application or obtain a client_id before using the CLI. Dynamic client registration handles this automatically on every login.

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.

On this page