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 wiring — app_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 storagecheck 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:
- Auto-selects a free loopback port from the registered range
(
https://localhost:3764/…:3769/) and prints the chosen callback. - Prints (and tries to open) the authorize URL with the scopes keryx needs,
including
instagram_business_content_publish. - Captures the returned
codefrom whichever comes first: - 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. - a code pasted on stdin — the fallback for a box the browser can't reach. Both paths run together; first to arrive wins.
- Exchanges
code → short-lived → long-lived(~60-day) token. - 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 acodeyou copied from the browser's address bar directly. Pair with--redirectset to the same URI the code was issued for.
Prerequisites¶
platforms.instagram.app_idset (the Instagram app ID).INSTAGRAM_APP_SECRETin 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-
httpsredirect, regardless of host, so register thehttpsrangehttps://localhost:3764/…https://localhost:3769/. keryx picks the first free one; the range must stay in sync withloopbackPortRangein 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:
- Open the authorize URL, approve, and let the browser land on
https://localhost:<port>/?code=…#_(it may show connection refused — thecodeis still in the address bar). - Copy the
code(betweencode=and#_) and runkeryx 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 keychain → config 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 port — no cert, no
warning, and nothing to pre-register.
- Auto-selects a free ephemeral
http://127.0.0.1:<port>/callback (or use--redirectto pin one). - Opens (or prints) the authorize URL with
access_type=offline+ consent prompt + PKCE so Google returns a refresh token. - Captures the
code(loopback or stdin paste), exchanges it (x/oauth2), and stores the refresh token: keychain on a desktop, else the config file. Writesplatforms.youtube.enabled(+channel_idwhen readable). Never prints the token.
Prerequisites¶
platforms.youtube.client_idset — the Google OAuth Desktop app client id.YOUTUBE_CLIENT_SECRETin 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 env →
OS keychain → config 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.)
- Auto-selects a free
http://127.0.0.1:<port>/callback/redirect (or use--redirectto pin one) and prints/opens the authorize URL with PKCE — note TikTok's quirks: the params useclient_key(notclient_id) and the PKCEcode_challengeis hex-encoded SHA256 (not base64url), methodS256. - Captures the
code(loopback or stdin paste) — no hosted page needed. - 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_keyset — the TikTok app client key.TIKTOK_CLIENT_SECRETin the environment — the client secret.- A Desktop loopback redirect registered (e.g.
http://127.0.0.1:*/callback/, wildcard port), scopesuser.info.basic+video.publish, and Direct Post enabled. Posts areSELF_ONLYuntil 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 env →
OS keychain → config 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).
- Auto-selects a free
http://127.0.0.1:<port>/redirect (or use--redirect; if LinkedIn's portal rejects http, pin an https one). - 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. Writesplatforms.linkedin.{author_urn, enabled}. Never prints the token.
Prerequisites¶
platforms.linkedin.client_idset;LINKEDIN_CLIENT_SECRETin 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 env →
OS keychain → config 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.