Skip to content

Add OAuth service accounts and RFC 7592 client management - #619

Closed
Caushi wants to merge 5 commits into
mainfrom
feature/oauth-service-accounts-and-client-management
Closed

Add OAuth service accounts and RFC 7592 client management#619
Caushi wants to merge 5 commits into
mainfrom
feature/oauth-service-accounts-and-client-management

Conversation

@Caushi

@Caushi Caushi commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator
  • Add auth_method: client_credentials environments, so Bfabric.connect() authenticates a service account from a stored secret with no browser and no cached token
  • Add bfabric-cli auth service-account to record such an environment for cron jobs and scripts
  • Add bfabric-cli auth client-show, client-update and client-delete to inspect, correct and revoke a registered OAuth client (e.g. a wrong redirect URI) without re-registering it
  • Add --save-env to auth register and auth register-webapp, saving the new client plus the credentials needed to edit it later; it refuses an existing environment unless --force is passed, since saving would replace its stored credentials
  • Change auth status and auth list to report client_credentials environments as such, and auth logout to clear the stored client secret
  • Change api delete and api update to refuse to run without a terminal to confirm on, instead of hanging or failing on the prompt during an unattended run
  • auth client-update re-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 application weburl holds the same URL and needs api update application

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.
@Caushi
Caushi requested review from leoschwarz and a lite review from Copilot August 24, 2026 15:29
…-accounts-and-client-management

# Conflicts:
#	bfabric/docs/changelog.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_credentials using an inline client_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.

Comment on lines +40 to +42
_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."""
Comment on lines +130 to +132
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@leoschwarz

Copy link
Copy Markdown
Member

#596 arrived on main and I made some small changes while merging main into this branch.

@Caushi

Caushi commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

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 thread BaseUrl through the registration save path commit and unanchored the reviews. Your commit is cherry-picked into #622 with authorship intact.

The substantive change during the rebase: the four merge_auth_owned_keys call sites move to write_environment_to_config(auth="merge") and read_environment_auth_keys, since #621 replaces that helper with an explicit mode. Also auth client-delete now clears auth_method too — #621's write-side validation refuses to persist client_credentials without a secret, which is exactly what deleting the client leaves behind.

Happy to close whichever of the two you prefer; #622 targets refactor/auth-method-union, so it needs #621 to land first.

@leoschwarz leoschwarz closed this Aug 25, 2026
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.

3 participants