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)

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.