Skip to content

Add a command that prints a Connect access token for other tools #21

Open

Description

@nealrichardson

Summary

Add a command that prints a valid Connect access token for a saved server, refreshing it first if needed. Other tools could then use the credentials from posit connect login without reading posit-cli's credential store themselves.

posit connect token [-s URL | -n NAME] [--format json]

Motivation

posit connect login makes OAuth the recommended way to sign in to Connect, so users don't have to create and manage API keys. The Connect SDKs (posit-sdk-py and connectapi) still require an API key, so a user who signed in with posit connect login has to create one separately to use them.

The SDKs could read the stored credentials directly, but that would be fragile:

  • The storage format is internal. Tokens live in the system keyring (service rsconnect-python, username {url}:access_token / {url}:refresh_token), with oauth_* fields in servers.json as a fallback. Neither is a public contract, and both would change if the CLI is rewritten (see Build a standalone binary CLI to replace the Python and R publishing clients #19).
  • Refresh tokens rotate. After a refresh, the CLI writes back the new refresh token. If another tool refreshed on its own without writing back with the same keyring and file behavior, the CLI's stored token would stop working, or the other way round. Two writers would also race.
  • R can't easily read a keyring entry written by Python. connectapi would need its own copy of the lookup logic.

A command avoids all three. The CLI stays the only thing that reads, refreshes, and writes stored credentials, and any language can call it. This is the same pattern as gh auth token, gcloud auth print-access-token, git credential helpers, and kubectl exec plugins.

Proposed behavior

  • Server selection works like other commands: -s/--server URL or -n/--name NAME, falling back to CONNECT_SERVER.

  • If the stored access token is expired or close to expiring, refresh it and persist the new tokens before printing.

  • Default output is the bare access token on stdout, so $(posit connect token) works in shell scripts.

  • --format json prints machine-readable output, for example:

    {"access_token": "...", "token_type": "Bearer", "expires_at": 1793000000, "server": "https://connect.example.com"}

    expires_at lets callers call the command again before the token expires instead of waiting for a 401.

  • Print nothing on stdout and exit non-zero when there are no stored OAuth credentials for the server, or when the refresh fails (for example, the grant was revoked). The error message on stderr should say to run posit connect login. Distinct exit codes for "not logged in" and "refresh failed" would let callers give better messages.

  • Never prompt or start an interactive login. Callers are often non-interactive (scripts, notebooks, SDKs).

  • Servers registered with an API key: either print nothing and exit non-zero, or provide a separate opt-in. Printing API keys by default seems riskier than printing short-lived access tokens.

How the SDKs would use it

When the user doesn't pass an API key and CONNECT_API_KEY isn't set, the SDK runs posit connect token -s $CONNECT_SERVER --format json and sends Authorization: Bearer <access_token>. It runs the command again shortly before expires_at, and once more after a 401. An API key always takes precedence, which matches how posit-cli itself handles precedence. This keeps CI and content running on Connect working unchanged.

Open questions

  • Command name: token, auth token, or print-token?
  • Should the JSON output schema and exit codes be documented as a stable interface, so the SDKs can rely on them across posit-cli versions (including a possible rewrite)?
  • Concurrency: if several processes call the command at the same time while a refresh is due, should the CLI lock the store so only one refresh happens?

Activity

  1. nealrichardson commented on Oct 26, 2026

    @nealrichardson
    ContributorAuthor

    Plan, after some discussion:

    Where it lives: rsconnect-python, in posit-dev/rsconnect-python#884. All the code for storing and refreshing tokens is already there, and the command needs a refresh that's both public and locked, which only rsconnect can provide without becoming a second writer to the store. posit-cli mounts rsconnect's commands, so this becomes posit connect token once posit-cli moves to a release that includes it. If we later want different behavior, posit-cli can register its own command under the same name.

    No expiry tracking. Callers have to handle a 401 anyway, because tokens can be revoked, so checking expiry ahead of time would only save one request per token lifetime. It would also need a change to how rsconnect stores credentials, since with the keyring the expiry isn't saved. Instead:

    • posit connect token prints the stored token, without contacting the server.
    • posit connect token --refresh forces a refresh, saves the rotated tokens, and prints the new access token.
    • --format json prints {"access_token", "token_type": "Bearer", "server"}.
    • Exit status: 0 on success, 4 when you need to log in again (no stored OAuth credentials, or the server rejected the refresh token), and 1 for other errors. Nothing goes to stdout on failure. API keys are never printed, and the command never prompts.

    Concurrency: refreshes run under a lock shared across processes. If the stored token has already changed from the one the server rejected, another process already refreshed, and that token is returned without another exchange. rsconnect's own 401 retry uses the same code, so concurrent deploys also stop invalidating each other's refresh tokens.

    SDK flow: if no API key is set, run posit connect token -s $CONNECT_SERVER --format json and send Authorization: Bearer <token>. On a 401, run it again with --refresh and retry once. If you get another 401 or exit status 4, tell the user to run posit connect login. Retrying means the request body has to be re-sendable, for example for streamed uploads.

    Remaining work in posit-cli once rsconnect releases this: bump the rsconnect pin, add token to EXPECTED_RSCONNECT_COMMANDS, and mention it in the README.

  2. tdstein commented on Oct 26, 2026

    @tdstein
    Contributor

    posit-dev/rsconnect-python#884 is merged, but is unreleased

    @nealrichardson what's your usual pattern here? Do you release or do you just update the deps chain to point at https://github.com/posit-dev/rsconnect-python?

  3. nealrichardson commented on Oct 26, 2026

    @nealrichardson
    ContributorAuthor

    what's your usual pattern here? Do you release or do you just update the deps chain to point at https://github.com/posit-dev/rsconnect-python?

    Not sure we've done this enough to have a usual pattern yet. We could update the pyproject.toml to point to github until the rsconnect-python release, then revert that when we release this. Which we will have to do before the Connect release in 2 weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions