Skip to content

feat(auth): add self-refreshing token-file auth mode#200

Open
npstewart87 wants to merge 3 commits into
XeroAPI:mainfrom
npstewart87:feat/token-file-self-refresh
Open

feat(auth): add self-refreshing token-file auth mode#200
npstewart87 wants to merge 3 commits into
XeroAPI:mainfrom
npstewart87:feat/token-file-self-refresh

Conversation

@npstewart87

Copy link
Copy Markdown

What this adds

A third auth mode, XERO_TOKEN_FILE, that reads an OAuth2 token store and self-refreshes via the rolling refresh token. Free, region-agnostic, persistent — no Custom Connection, no 30-minute bearer-token expiry.

Why

For a self-hosted/local user (Claude Desktop/Code) both existing modes fall short:

  • Bearer token is a static string with no refresh — Xero access tokens die after ~30 min, so the server returns 401s mid-session with no recovery (Bearer token mode should accept a refresh token and self-refresh #163).
  • Custom Connections is the only auto-refreshing mode, but it's a paid add-on and region-restricted (AU/NZ/UK/US), so users elsewhere have no working option for long sessions at all.

This closes the gap using the standard authorization-code + offline_access flow every Xero app already supports — in every region, for free.

Behavior

Notes

Same gap raised in #163 and previously attempted in #140 / #120. I'd genuinely appreciate a maintainer's position: if there's an architectural objection, I'm happy to adapt — but if the only blocker is that free self-refresh overlaps with the paid Custom Connection, please say so explicitly, because today non-AU/NZ/UK/US users have no working long-session auth at all.

Tested locally against a US org via Claude Code — full read + write, including bank transactions.

Adds a third auth mode (XERO_TOKEN_FILE) that reads an OAuth2 token store
from disk and renews the access token via the rolling refresh token when
near expiry, persisting the rotated token back atomically with 0600 perms.

This gives self-hosted/local users persistent, free authentication that
works in every region — without a paid Custom Connection and without the
static bearer token expiring mid-session (~30 min). authenticate() runs at
the start of every handler, so each tool call gets a valid token.

Optional XERO_TENANT_ID pins a single org when the token has multiple
tenants connected; otherwise the first connected tenant is used.

Refs XeroAPI#163.
Extends the XERO_TOKEN_FILE mode so the initial token file no longer has to
be produced by hand. A new `auth` subcommand runs the full OAuth2
authorization-code flow itself: it opens the browser to Xero's authorize URL,
catches the redirect on a one-shot local callback server, exchanges the code
(HTTP Basic client auth), and writes the token store the self-refreshing
client consumes. After this, RefreshingTokenXeroClient keeps it alive.

- src/consts/auth.ts: endpoints + resolveScopes(). XERO_SCOPES is required
  (no built-in default — avoids silently over/under-granting); offline_access
  is appended automatically since it yields the refresh token. Default
  redirect port is 53682, not 5000 (macOS AirPlay Receiver owns 5000).
- src/auth/token-store.ts: shared readTokens/persistTokens/stampExpiry so the
  bootstrap and the refreshing client agree on the on-disk shape and cannot
  drift. xero-client.ts now uses these helpers.
- src/auth/bootstrap-auth.ts: runAuthorizationCodeFlow() — state/CSRF check,
  5-minute callback timeout, cross-platform browser open with manual-URL
  fallback, and a tenant listing so multi-org users can set XERO_TENANT_ID.
- src/index.ts: `auth` argv branch, lazily importing the bootstrap so a normal
  run leaves the stdio transport untouched.
- README: document the command, required scopes, and the one-time redirect-URI
  prerequisite on the Xero app.
@tobiasmuehl

Copy link
Copy Markdown

Love this!! I went ahead and also implemented the process to generate the initial Xero token file. The PR for that is posted on the fork repo: npstewart87#1

feat(auth): end-to-end OAuth bootstrap (`auth` command) for token-file mode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants