Add OAuth service accounts and RFC 7592 client management - #619
Conversation
Adds an `auth_method: client_credentials` environment that authenticates from an inline `client_secret`, so cron jobs and scripts connect without a browser or a cached token, and `bfabric-cli auth service-account` to record one. Registration responses can now be saved with `auth register --save-env`, including the RFC 7592 `registration_access_token` / `registration_client_uri`. That makes `auth client-show` / `client-update` / `client-delete` possible, so a client registered with a wrong redirect URI is corrected in place rather than re-registered. Two write paths need care, both verified against the live TEST instance: B-Fabric rotates *both* the client secret and the registration access token on every successful update and revokes the old ones immediately (confirmed: the previous token returns 401). Since the config writer replaces every auth-owned key at once, writing only the rotated keys would drop the untouched ones and strand the client unmanageable. `merge_auth_owned_keys` carries the rest along. Rotation is detected by comparing against the stored value rather than testing for presence, so a server echoing an unchanged credential reports nothing. `--save-env` into an existing environment would likewise replace its stored credentials, so it now refuses unless `--force` is passed. Also refuses `api delete` / `api update` without a terminal to confirm on: unattended service-account runs would otherwise hang or die on a prompt.
…-accounts-and-client-management # Conflicts: # bfabric/docs/changelog.md
There was a problem hiding this comment.
Pull request overview
This PR extends bfabricPy’s OAuth support to cover unattended service accounts via client_credentials, and adds RFC 7592 client management capabilities to the library and bfabric-cli so registered OAuth clients can be inspected/updated/deleted without re-registering.
Changes:
- Add config + connection support for
auth_method: client_credentialsusing an inlineclient_secret(no browser / no token cache). - Add CLI commands to save service-account environments and to manage OAuth clients via RFC 7592 (
client-show,client-update,client-delete), including persistence of rotated registration credentials. - Improve CLI non-interactive behavior for destructive operations (
api update/api delete) and update docs + changelogs accordingly.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/bfabric/test_bfabric.py | Adds unit coverage for client_credentials config-based connect behavior and multi-env selection. |
| tests/bfabric/oauth/test_registration.py | Adds tests for RFC 7592 client read/update/delete helpers. |
| tests/bfabric/config/test_config_writer.py | Adds coverage ensuring client_secret is treated as auth-owned and cleared appropriately. |
| tests/bfabric/config/test_config_file.py | Adds parsing + secret-leak prevention tests for client_credentials and registration credential fields. |
| tests/bfabric/config/test_config_data.py | Adds round-trip tests ensuring client_secret survives config load/export/import. |
| tests/bfabric_scripts/cli/login/test_cmd_login_register.py | Adds coverage for --save-env behavior on auth register, including overwrite protection. |
| tests/bfabric_scripts/cli/login/test_cmd_login_register_webapp.py | Adds coverage for auth register-webapp --save-env and warnings when registration credentials are missing. |
| tests/bfabric_scripts/cli/login/test_cmd_auth_status.py | Ensures auth status reports client_credentials correctly without leaking secrets. |
| tests/bfabric_scripts/cli/login/test_cmd_auth_service_account.py | New tests for bfabric-cli auth service-account config writing and secret rotation behavior. |
| tests/bfabric_scripts/cli/login/test_cmd_auth_client.py | New tests for RFC 7592 CLI client management commands and config persistence/cleanup. |
| tests/bfabric_cli/test_cli_api_update.py | Adds tests for refusing non-interactive updates without --no-confirm. |
| bfabric/src/bfabric/oauth/_registration.py | Implements RFC 7592 read/update/delete helpers on top of httpx. |
| bfabric/src/bfabric/oauth/init.py | Exposes RFC 7592 helper functions from the oauth package. |
| bfabric/src/bfabric/config/config_writer.py | Expands auth-owned/inline-secret key handling and adds merge helper for partial auth updates. |
| bfabric/src/bfabric/config/config_file.py | Extends environment schema to include client_credentials + secrets + RFC 7591 registration credentials. |
| bfabric/src/bfabric/config/config_data.py | Carries client_secret through ConfigData and its JSON export path. |
| bfabric/src/bfabric/bfabric.py | Makes Bfabric.connect() support client_credentials environments via _connect_oauth_from_config. |
| bfabric/docs/user_guides/connecting/server_webapp_usage.md | Clarifies identity semantics of client_credentials tokens. |
| bfabric/docs/user_guides/bfabric-cli/authentication.md | Documents service-account usage, env saving, and RFC 7592 client management CLI commands. |
| bfabric/docs/getting_started/configuration.md | Adds a config-file example for service-account environments. |
| bfabric/docs/design/oauth_usage_and_troubleshooting.md | Updates guidance around client_credentials identity and mentions new CLI flows. |
| bfabric/docs/changelog.md | Adds unreleased changelog entries for new OAuth capabilities. |
| bfabric_scripts/src/bfabric_scripts/cli/login/service_account.py | Implements bfabric-cli auth service-account command. |
| bfabric_scripts/src/bfabric_scripts/cli/login/register.py | Adds --save-env / --force support and persists registration results. |
| bfabric_scripts/src/bfabric_scripts/cli/login/register_webapp.py | Adds --save-env / --force support for webapp registration. |
| bfabric_scripts/src/bfabric_scripts/cli/login/manage.py | Updates auth-status method resolution to include client_credentials. |
| bfabric_scripts/src/bfabric_scripts/cli/login/client_manage.py | Implements RFC 7592 client show/update/delete commands with confirmation and config updates. |
| bfabric_scripts/src/bfabric_scripts/cli/login/_common.py | Adds shared save_registration() helper plus --save-env / --force help text. |
| bfabric_scripts/src/bfabric_scripts/cli/cli_auth.py | Registers new auth subcommands (service-account + client-*). |
| bfabric_scripts/src/bfabric_scripts/cli/api/update.py | Refuses interactive confirmation prompts when no terminal is available (suggests --no-confirm). |
| bfabric_scripts/src/bfabric_scripts/cli/api/delete.py | Refuses interactive confirmation prompts when no terminal is available (suggests --no-confirm). |
| bfabric_scripts/docs/changelog.md | Adds unreleased changelog entries for new CLI authentication and safety behaviors. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _INLINE_SECRET_KEYS: tuple[str, ...] = ("login", "password", "pat", "client_secret") | ||
| """``registration_access_token`` is deliberately absent: it authenticates *managing* the client, not | ||
| calling the API, so a logout that dropped it would strand a misconfigured client with no way to fix it.""" |
| A browser login is wrong for a script that nobody is watching: its token expires and there is no one | ||
| to re-login. A **service account** authenticates from a stored secret instead, so it never expires | ||
| and needs no browser. |
| - `BROWSER=/bin/true bfabric-cli auth login` stops a *terminal* browser (`w3m`, `lynx`, …) from | ||
| hijacking the login and rendering the page into your shell. | ||
|
|
||
| ## Unattended scripts and cron jobs |
There was a problem hiding this comment.
Something for a follow up PR, but i think we should split the file according to the two modes:
- Basic user, pkce only, 1 bfabric instance.
- Advanced use cases covered in more detail.
| auth_method: Literal["password", "oauth", "pat"] | None = None | ||
| auth_method: Literal["password", "oauth", "pat", "client_credentials"] | None = None | ||
| client_id: str | None = None | ||
| client_secret: SecretStr | None = None |
There was a problem hiding this comment.
I now realise that ConfigData has grown beyond what it was supposed to entail originally, just a container for the individual members BfabricClientConfig and BfabricAuth. There's nothing wrong with the code but it feels like ConfigData has taken on the responsibility that in the past BfabricAuth had (there are good reasons but just some thoughts I had just now when reading this).
| # Inline secrets cleared by :func:`clear_environment_credentials`. OAuth is absent: its token lives in | ||
| # the file cache, so clearing here would report success while leaving the credential in place. | ||
| _INLINE_SECRET_KEYS: tuple[str, ...] = ("login", "password", "pat") | ||
| _AUTH_OWNED_KEYS = frozenset( |
There was a problem hiding this comment.
This set is duplicated in bfabric/src/bfabric/config/config_file.py, would be great to define it once and reuse (it's a list there but used as a set).
The non-interactive confirmation guard for `api update` / `api delete` is unrelated to OAuth service accounts and shipped here only by proximity. It now lives on fix/api-noninteractive-confirm so both changes can be reviewed on their own.
| if is_service_account and result.get("client_secret"): | ||
| env_data["auth_method"] = "client_credentials" | ||
| # Carry over auth-owned keys this response does not supply; the writer replaces them wholesale. | ||
| merged = merge_auth_owned_keys(config_file, env_name, env_data) |
There was a problem hiding this comment.
Wouldn't it be better to handle it in config_writer instead?
| ) -> dict[str, object]: | ||
| """Send an RFC 7592 client-management request authorized by the registration access token.""" | ||
| logger.debug("Client management {} on {}", method, registration_client_uri) | ||
| with raise_if_unavailable(registration_client_uri): |
There was a problem hiding this comment.
There's some code duplication now (the flow with logger.debug, with raise_if_unavailable, request, and raise for status), could be cleaned up in the whole module.
|
|
||
| ### Added | ||
|
|
||
| - `bfabric-cli auth service-account` records a `client_credentials` OAuth client, so cron jobs and shell scripts authenticate without a browser or a cached token. Every command then works unattended, picking the instance with `--config-env`. Re-run it to store a secret rotated in the B-Fabric UI; the environment's other recorded values are kept. |
There was a problem hiding this comment.
Should we merge the 3 new commands behind a single subcommand?
bfabric-cli auth client {create, show, delete}
| report a rotation that never happened. | ||
| """ | ||
| stored = merge_auth_owned_keys(config_file, env_name, {}) | ||
| rotated = { |
There was a problem hiding this comment.
Could rotation be handled in the config_writer.py instead?
…-accounts-and-client-management Conflicts: - bfabric_scripts/docs/changelog.md: keep both Unreleased sections, with the base-url bullet folded into Changed and the subsections ordered Added / Changed / Fixed as in the released entries. - tests/bfabric/test_bfabric.py: keep main's BaseUrl pickle round-trip test inside TestPickling alongside the branch's new TestClientCredentialsMultiInstance. Adapted to main's canonical base URL (no trailing slash, must end in /bfabric): the client-credentials config-writer tests used a bare https://example.com, which BaseUrl now rejects, and one docs example still carried a trailing slash.
…ve path save_registration and _resolve_token_from_config took a plain str, so a BaseUrl was cast back down to str at the two call sites and then re-normalised inside save_registration. Type both on BaseUrl and normalise once, where the CLI argument enters. Also drops an inconsistency in cmd_login_register, which passed the normalised URL to register_client but the raw one to save_registration.
|
#596 arrived on main and I made some small changes while merging main into this branch. |
|
Opened #622 as a rebase of this work onto #621 (the auth-method union refactor), rather than force-pushing here — that would have dropped your The substantive change during the rebase: the four Happy to close whichever of the two you prefer; #622 targets |
auth_method: client_credentialsenvironments, soBfabric.connect()authenticates a service account from a stored secret with no browser and no cached tokenbfabric-cli auth service-accountto record such an environment for cron jobs and scriptsbfabric-cli auth client-show,client-updateandclient-deleteto inspect, correct and revoke a registered OAuth client (e.g. a wrong redirect URI) without re-registering it--save-envtoauth registerandauth register-webapp, saving the new client plus the credentials needed to edit it later; it refuses an existing environment unless--forceis passed, since saving would replace its stored credentialsauth statusandauth listto reportclient_credentialsenvironments as such, andauth logoutto clear the stored client secretapi deleteandapi updateto refuse to run without a terminal to confirm on, instead of hanging or failing on the prompt during an unattended runauth client-updatere-saves the client secret and registration token that B-Fabric rotates on each update, so the client stays manageable across repeated edits. It changes the OAuth client only: a webapp's applicationweburlholds the same URL and needsapi update application