Security model
polar-flow-mcp drives an unofficial, reverse-engineered API with session-cookie auth. This page documents the threat model, what is and isn’t protected, and the operational practices to follow.
Threat model
Section titled “Threat model”Trusted
Section titled “Trusted”- The host running the server (process memory, the cookie-jar file).
- The operator’s environment file (
.env). - Polar’s TLS stack (
flow.polar.com,auth.polar.com).
Untrusted
Section titled “Untrusted”- Anyone with access to the host filesystem outside of the process owner.
- Anyone who can read the binary’s environment (other users on a shared host,
processes that can inspect
/proc/<pid>/environ). - The network path to
*.polar.com(mitigated by TLS).
Out of scope
Section titled “Out of scope”- MFA / captcha — the test accounts used during development have neither. An MFA-enabled account will fail the headless login chain; document this to your users.
- Polar’s own session security — we treat
FLOW_SESSIONas an opaque bearer token issued by Polar.
What’s protected
Section titled “What’s protected”- The Polar password never touches disk. It lives only in the process environment + memory, and is used only to perform the initial login.
- The cookie-jar file is chmod 0600. Only the running user can read or write
it. We write atomically (
.tmpthen rename) to avoid partial state. /healthzintentionally returns no information about session state — it does not let an unauthenticated observer infer whether the server is logged in.
What’s not protected
Section titled “What’s not protected”- The cookie jar is plaintext. It contains
FLOW_SESSION(JWT) andremember-me. Either is a bearer credential equivalent to a short-term password for the Polar account. Treat the file like an SSH key: protect it with filesystem permissions and back it up only encrypted. - The
.envfile is whatever you make it. It contains the Polar password in plaintext by design. Use restrictive permissions and avoid committing it. - The HTTP transport has no auth by default. With neither
MCP_API_KEYnorOAUTH_PUBLIC_URLset, bind to localhost or put it behind a trusted-network barrier (Tailscale, VPN). A warning is logged at startup ifBIND_ADDRESSis non-localhost and inbound auth is off. To expose it publicly, enable either the static API key or the OAuth Authorization Server (below).
Exposing publicly: static API key
Section titled “Exposing publicly: static API key”The low-ceremony option. Set MCP_API_KEY to a secret of at least 24 characters
and every /mcp request must present it as Authorization: Bearer <key>. The
server stores only the SHA-256 of the configured key and compares digests in
constant time, so neither the value nor its length leaks through timing; the key
never appears in a log line or a response body.
What this posture does not give you, and the reason OAuth still exists here:
- No identity. Every caller is the same anonymous holder of one secret. Logs cannot attribute a request to a person.
- No expiry, coarse revocation. The key lives until you change it, and changing it means a restart that breaks every existing client at once. OAuth’s access tokens expire on their own and can be cut off by rotating the signing key.
- Full compromise on leak. A key pasted into the wrong shell history, CI log, or client config grants everything the MCP surface exposes — including the write tools. OAuth confines a leaked access token to its TTL.
- Not a Claude.ai connector. Claude.ai’s connector UI drives an OAuth flow; it has nowhere to put a static header. This posture is for Claude Code and scripted clients.
It is a reasonable trade when you are the only caller, the deployment is behind
TLS, and standing up Authelia is more machinery than the situation warrants.
MCP_API_KEY and OAUTH_PUBLIC_URL are mutually exclusive: running both would
let the weaker mechanism answer for the stronger one, so the server refuses to
start.
Exposing publicly: app-as-Authorization-Server
Section titled “Exposing publicly: app-as-Authorization-Server”To reach the server from Claude.ai web/mobile and Claude Code, enable inbound
OAuth by setting OAUTH_PUBLIC_URL (+ OAUTH_ALLOWED_EMAIL +
OAUTH_TRUSTED_PROXIES). The server then becomes its own OAuth 2.1
Authorization Server:
- Serves RFC 9728 / RFC 8414 discovery metadata pointing clients back at itself.
- Implements Dynamic Client Registration (RFC 7591), so Claude self-registers — nothing to pre-create or paste.
- Delegates browser login on
/mcp/oauth/authorizeto a forward-auth proxy (Authelia), reads the authenticated email from a trusted header, and checks the email allowlist before issuing a PKCE-bound code. - Returns
401+WWW-Authenticateto unauthenticated/mcpcallers and validates every Bearer token locally (EdDSA JWT it signed).
The full walkthrough — including why forward-auth goes on /authorize only — is
in Expose the server securely.
The controls that matter
Section titled “The controls that matter”OAUTH_TRUSTED_PROXIES— the linchpin. The forward-auth identity header is honoured only from these peers, so a too-broad value lets a network neighbour spoof an identity. Scope it to your proxy/container network.OAUTH_ALLOWED_EMAIL— the allowlist of who may consent; checked again on every/mcpcall and refresh, so removing an email cuts access within the access-token TTL.- Redirect URIs are restricted to the Claude callbacks + loopback even though registration is open, so a rogue client can’t point authorization codes at an attacker URL.
OAUTH_ALLOWED_ORIGINS— opt-in DNS-rebinding defence (no-Origin requests are always allowed, so it never breaks Claude’s server-to-server calls).- Revocation is coarse: short
OAUTH_ACCESS_TTL_MINUTES, or rotate the signing key (deleteOAUTH_SIGNING_KEY_PATH+ restart) to invalidate every outstanding token at once.
What happens if the cookie jar leaks
Section titled “What happens if the cookie jar leaks”An attacker with the jar can act as the Polar account until either:
- The user changes their Polar password (invalidates
session_id/remember-meonauth.polar.com). - The user logs out everywhere from Polar’s UI.
- Polar’s
remember-mecookie naturally expires (~14 days from last use).
There is no way to revoke a single FLOW_SESSION short of those actions — this
is Polar’s design, not ours. The mitigation is to keep the jar file strictly
readable by the running user and avoid keeping backups.
What happens if .env leaks
Section titled “What happens if .env leaks”The attacker has full Polar account access — they can change the password, which
then locks the legitimate user out and gives the attacker control of all health
data and settings. This is strictly worse than a cookie-jar leak. Protect
.env accordingly.
Recommended deployment posture
Section titled “Recommended deployment posture”- Run as a non-root user.
- Mount the cookie jar on a volume that the host’s other users / containers can’t read.
- For local/private use, bind the HTTP transport to
127.0.0.1and consume via SSH tunnel or Tailscale. - For public use, front the server with a TLS reverse proxy and enable inbound
auth:
MCP_API_KEYwhen you are the only caller, or the OAuth Authorization Server for Claude.ai and multi-user access — see Expose the server securely. - Use a Polar account dedicated to MCP use, not your main one.
Reporting issues
Section titled “Reporting issues”Security issues: open a private security advisory via the GitHub Security tab on the repo. Do not file public issues for live credentials or session bypasses.