Skip to content

keryx auth

Run a platform's interactive OAuth flow and store its access token, so posting "just works" without hand-cranking the API. Instagram is the first platform.

keryx auth <platform>                          # e.g. keryx auth instagram
keryx auth instagram --redirect <uri>          # pin a specific registered redirect
keryx auth instagram --redirect <uri> --code <code>   # exchange a pasted code (non-interactive)

Where the token is stored

Every platform stores its credential the same way: the OS keychain when a desktop secret service is available, otherwise the config file.

On a headless box or in CI there is no keychain, so the config file is the normal path rather than an edge case — and keryx names the exact file it wrote to:

✓ Authorised Instagram (user_id 12345).
  Token stored in /home/you/.keryx/config.yaml (PLAINTEXT — do not commit this file); valid ~60 days.

Credentials belong in user config, not a project's .keryx.yaml. A project config is committed, so a token in it is a token in your git history. What a project should carry is the wiringapp_id, user_id, client_id, open_id, channel_id, author_urn — which says which account it posts to without saying how to authenticate as it.

doctor checks the project file for you

keryx doctor reads the .keryx.yaml in the current directory and reports any credential-shaped key carrying a value:

$ keryx doctor
  [!!] Project credentials: 3 literal credentials in .keryx.yaml
       Key(s): platforms.instagram.access_token, platforms.tiktok.refresh_token,
       platforms.youtube.refresh_token. They are in /home/you/blog/.keryx.yaml, which is
       committed to the repository — move them to your user config (`keryx auth <platform>`
       writes there) or supply them from the environment.

It reports key names only, never values — doctor output gets pasted into issues and support threads. It matches on the shape of a key rather than a fixed list, so a platform keryx does not know about yet, or a key you added by hand, is still caught. No project file in the current directory is reported as a skip, not a pass: nothing was examined, so nothing is being vouched for.

There is a separate, GTB-provided Credential storage check in the same report. It covers GTB's own credential keys (Anthropic, GitHub, GitLab, …) and knows nothing about keryx's platform tokens, so it can read [OK] while this check reports findings. They are looking at different things.

Instagram

keryx auth instagram runs the Instagram Login OAuth flow — no --redirect needed in the common case:

  1. Auto-selects a free loopback port from the registered range (https://localhost:3764/:3769/) and prints the chosen callback.
  2. Prints (and tries to open) the authorize URL with the scopes keryx needs, including instagram_business_content_publish.
  3. Captures the returned code from whichever comes first:
  4. a loopback callback server bound to all interfaces on the chosen port — so the browser reaches it via localhost, an SSH port-forward, or the host's LAN IP. It auto-captures the code (nothing to copy). Meta requires an https redirect, so the server serves TLS with an in-memory self-signed cert; the browser warns once that the cert is untrusted — proceeding is safe (a local-only, throwaway cert). See Certificate trust.
  5. a code pasted on stdin — the fallback for a box the browser can't reach. Both paths run together; first to arrive wins.
  6. Exchanges code → short-lived → long-lived (~60-day) token.
  7. Stores the token: the OS keychain on a desktop, else the config file (a headless dev server / CI has no keychain). Writes platforms.instagram.user_id, redirect_uri + enabled. The token is never printed.

Flags

  • --redirect <uri> — pin a specific OAuth redirect URI instead of auto-selecting. Must be one of the URIs registered in the Meta app. Use this to force a port, or for a non-loopback redirect.
  • --code <code> — skip the interactive step and exchange a code you copied from the browser's address bar directly. Pair with --redirect set to the same URI the code was issued for.

Prerequisites

  • platforms.instagram.app_id set (the Instagram app ID).
  • INSTAGRAM_APP_SECRET in the environment (the Instagram app secret).
  • The loopback redirect URIs registered in the Meta app (API setup with Instagram login → Business login settings). Meta rejects every non-https redirect, regardless of host, so register the https range https://localhost:3764/https://localhost:3769/. keryx picks the first free one; the range must stay in sync with loopbackPortRange in the code.

Typical run

keryx auth instagram
# → Using OAuth callback https://localhost:3764/
# → opens the authorize URL; approve consent (lists content publishing)
# → browser lands on https://localhost:3764/?code=… ; click through the
#   one-time cert warning; keryx captures + stores the token automatically.

On a remote box, port-forward first (ssh -L 3764:localhost:3764 …) or just hit the host's LAN IP — the callback binds all interfaces.

Manual fallback (--code)

If the browser can't reach the callback at all, keryx also prints the URL and waits for a pasted code:

  1. Open the authorize URL, approve, and let the browser land on https://localhost:<port>/?code=…#_ (it may show connection refused — the code is still in the address bar).
  2. Copy the code (between code= and #_) and run keryx auth instagram --redirect https://localhost:<port>/ --code <code>, using the same port the authorize URL used.

The code is single-use and expires in minutes — grab it and exchange promptly.

Certificate trust

Today the callback presents an in-memory self-signed cert, so the browser shows a one-time "untrusted certificate" warning — safe to click through, since the cert is local-only and regenerated each run. A future (pre-v1) enhancement will add a keryx init hook to install a phpboyscout-signed trusted root CA (opt-in), so the callback cert is trusted and the warning disappears.

See the how-to: Get Instagram posting credentials.

Token resolution

The Instagram publisher reads its token in this order: INSTAGRAM_ACCESS_TOKEN env (CI / manual override) → OS keychainconfig file. auth refresh (rotate before the ~60-day expiry, alert on failure) is the next piece.

YouTube

keryx auth youtube runs Google's OAuth flow and stores a refresh token (the durable secret; access tokens are minted from it on demand). It reuses the same capture machinery (pkg/oauth) as Instagram, but Google is RFC 8252-compliant, so the callback is a plain-http loopback on any free portno cert, no warning, and nothing to pre-register.

  1. Auto-selects a free ephemeral http://127.0.0.1:<port>/ callback (or use --redirect to pin one).
  2. Opens (or prints) the authorize URL with access_type=offline + consent prompt + PKCE so Google returns a refresh token.
  3. Captures the code (loopback or stdin paste), exchanges it (x/oauth2), and stores the refresh token: keychain on a desktop, else the config file. Writes platforms.youtube.enabled (+ channel_id when readable). Never prints the token.

Prerequisites

  • platforms.youtube.client_id set — the Google OAuth Desktop app client id.
  • YOUTUBE_CLIENT_SECRET in the environment — the client secret.
  • The OAuth app published to "In production" (else refresh tokens expire in 7 days). Uploads stay private until the app passes Google's audit. See the how-to: Get YouTube posting credentials.

Token resolution

The YouTube publisher reads its refresh token YOUTUBE_REFRESH_TOKEN envOS keychainconfig file, and mints access tokens from it via x/oauth2 (YouTube's refresh token doesn't rotate, so refresh = keep it used).

TikTok

keryx auth tiktok runs TikTok's OAuth flow and stores a refresh token (the durable secret). As a Desktop app, keryx uses a loopback redirect, so it auto-captures the code on a free http://127.0.0.1:<port>/ callback — the same flow as YouTube — with stdin paste as the headless fallback. (Only TikTok Web apps are barred from loopback; Desktop apps require it.)

  1. Auto-selects a free http://127.0.0.1:<port>/callback/ redirect (or use --redirect to pin one) and prints/opens the authorize URL with PKCE — note TikTok's quirks: the params use client_key (not client_id) and the PKCE code_challenge is hex-encoded SHA256 (not base64url), method S256.
  2. Captures the code (loopback or stdin paste) — no hosted page needed.
  3. Exchanges it at TikTok's bespoke token endpoint and stores the refresh token: keychain on a desktop, else the config file. Writes platforms.tiktok.{open_id, enabled}. Never prints the token.

Prerequisites

  • platforms.tiktok.client_key set — the TikTok app client key.
  • TIKTOK_CLIENT_SECRET in the environment — the client secret.
  • A Desktop loopback redirect registered (e.g. http://127.0.0.1:*/callback/, wildcard port), scopes user.info.basic + video.publish, and Direct Post enabled. Posts are SELF_ONLY until the app passes TikTok's audit. See the how-to: Get TikTok posting credentials.

Token resolution — the rotating refresh token

The TikTok publisher reads its refresh token TIKTOK_REFRESH_TOKEN envOS keychainconfig file. TikTok access tokens last ~24h, so every post mints a fresh one from the refresh token — and TikTok may rotate the refresh token on that call, so keryx persists whatever comes back immediately (a dropped rotation locks the account out). This per-post mint-and-persist is the concrete shape auth refresh generalises across platforms.

LinkedIn

keryx auth linkedin runs LinkedIn's OAuth flow and stores the access token (~60 days). LinkedIn is a standard OAuth2 provider (client_id/client_secret), so it reuses the loopback capture (plain-http, like YouTube) and x/oauth2. It also reads the member's Person URN from /v2/userinfo (the post author).

  1. Auto-selects a free http://127.0.0.1:<port>/ redirect (or use --redirect; if LinkedIn's portal rejects http, pin an https one).
  2. Approve the permissions; keryx captures the code (loopback or paste), exchanges it, reads the Person URN, and stores the access token (keychain, else config) + its expiry. Writes platforms.linkedin.{author_urn, enabled}. Never prints the token.

Prerequisites

  • platforms.linkedin.client_id set; LINKEDIN_CLIENT_SECRET in the environment.
  • App on a verified Company Page with "Share on LinkedIn" + "Sign In with LinkedIn using OpenID Connect" products; scopes w_member_social + openid + profile. See the how-to: Get LinkedIn posting credentials.

Token resolution — the no-refresh case

The LinkedIn publisher reads its access token LINKEDIN_ACCESS_TOKEN envOS keychainconfig file, alongside a stored expiry. A standard app gets no refresh token (refresh is Marketing-Developer-Platform-only), so when the ~60-day token expires keryx cannot auto-refresh — it fails with a clear "re-run keryx auth linkedin" error, and auth refresh alerts before expiry (R-AUTH-4) rather than rotating. (If an MDP app ever returns a refresh_token, keryx stores and uses it automatically.) This is the fourth and final token shape auth refresh must handle.