OAuth Integration#
Overview#
Adds OAuth 2.0 support to bfabricPy. The library can now authenticate via PKCE, device code, client credentials, URL tokens, and personal access tokens — in addition to the existing password-based SOAP auth. All OAuth flows are transparent to downstream code: the SOAP engine receives BfabricAuth(login="__oauth__", password=<jwt>) automatically.
For task-oriented usage and troubleshooting (obtaining a working token, access_token vs id_token, the
containersclaim for file/download access, PKCE gotchas), see OAuth Usage & Troubleshooting.
New: bfabric.oauth module#
Package under bfabric/src/bfabric/oauth/ implementing all OAuth primitives.
The package root is the entire public surface: every submodule is underscore-prefixed, so the only supported import is from bfabric.oauth import .... __init__ exports OAuthCredentialProvider, pkce_login, AuthorizationRequest, exchange_code, device_code_login, token_url, register_client, register_webapp, TokenCache, compute_token_cache_path, UrlTokenContext and WebappClient — enough that nothing outside the package, including bfabric_scripts, ever names a module. Those names are provisional while the asgi-auth OAuth migration is in flight and may change without a deprecation cycle.
bfabric.py is the one place that names the private submodules, and only because __init__ re-exports WebappClient, whose module imports Bfabric — routing connect_* through the root makes that a static import cycle. It is an intra-package import, not a consumer reaching in.
Separately, those connect_* imports are function-local: the OAuth code pulls authlib and joserfc, and import bfabric must stay free of both. Note that targeting a submodule does not by itself avoid this — importing bfabric.oauth.<anything> executes __init__ first — so laziness, not module choice, is what keeps the base import clean.
File |
Purpose |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The core oauth API requires explicit client_id and scope arguments on all OAuth entry points. The library does not bake in a default client ID or scope. Tools like the CLI specify these values explicitly.
New: Factory methods on Bfabric#
Method |
Grant type |
Use case |
|---|---|---|
|
auto-detect |
Unchanged API. Now auto-detects |
|
client_credentials |
Service accounts / background jobs. |
|
authorization_code + PKCE |
Interactive browser login (programmatic). |
|
device_code |
Headless interactive login (programmatic). |
|
personal access token |
Opaque bearer token, no browser and no automatic refresh. |
|
URL token |
Webapps launched from B-Fabric. Returns |
|
URL token + client_credentials |
Dual-identity webapp client for apps launched from B-Fabric. |
New: CLI commands (bfabric-cli auth)#
All commands registered under bfabric-cli auth via cyclopts.
Command |
File |
Description |
|---|---|---|
|
|
Browser-based OAuth login. Caches tokens + writes config. Also registered top-level as |
|
|
Headless OAuth login. |
|
|
Personal Access Token login. |
|
|
RFC 7591 dynamic client registration. Outputs JSON. |
|
|
Registration preset for webapps (OIDC-inclusive scope). |
|
|
Show auth status for an environment. |
|
|
List environments grouped by instance, with scope / expiry / why-active. |
|
|
Make an environment the config default. |
|
|
Remove stored credentials for this machine, keeping the environment. |
|
|
Delete an environment: config entry plus cached tokens. |
See the CLI authentication guide for the user-facing view; this section covers only what is not visible from the outside.
All auth commands resolve the environment as --config-env > BFABRICPY_CONFIG_ENV >
GENERAL.default_config, matching ConfigFile.get_selected_config_env, and base_url is optional
for the login commands because it is read back from that environment. Commands that write to the
config refuse to run while BFABRICPY_CONFIG_OVERRIDE is set, since the file would not be the config
in effect.
auth logout removes credentials per auth method — the token cache for oauth, the inline pat or
login/password keys for the others (via clear_environment_credentials). It deliberately does not
revoke server-side. Instances do advertise revocation_endpoint in their discovery document (trace:
{base_url}/rest/oauth/token → /rest/oauth/revoke), but whether it is implemented is unverified, so
logout promises only what it delivers: local credentials gone, an issued token valid until it expires.
auth register enhancements#
--config-envreuses a cached OAuth token from an existing environment (no manual bearer token needed)--config-filespecifies the config file pathbase_urlis optional when--config-envis provided (inferred from config)Token resolution: explicit
--token> cached OAuth token via--config-env> interactive prompt
Config file changes#
New fields in environment config#
PRODUCTION:
base_url: "https://bfabric.example.com/bfabric"
auth_method: "oauth" # NEW — triggers OAuth flow in Bfabric.connect()
client_id: "CLI" # NEW — optional, defaults to "CLI"
scope: "api:write tus" # NEW — the scope *requested* at login
scope is what makes a login replayable from disk, and it is deliberately the requested value
rather than the granted one: the server drops scopes the client isn’t registered for, so replaying the
granted scope would bake that drop in permanently. Only the CLI reads the key — it is excluded from
BfabricClientConfig and not plumbed through ConfigData / export_config_data.
A re-login merges into an existing environment section rather than replacing it, so hand-written
keys (application_ids, engine, job_notification_emails, …) survive. What happens to the
auth-owned keys is the caller’s choice: write_environment_to_config(..., auth="replace") treats the
payload as the complete auth state, so a stale pat cannot outlive the method that wrote it, while
auth="merge" keeps the ones the payload does not mention, for a partial update such as a rotated
secret. The mode is required, because the two directions corrupt in opposite ways. The auth-owned key
set itself is derived from the auth-method variants in config/auth_methods.py.
In-memory model: the auth-method union#
The flat YAML above is the wire format (see the compatibility rules below for what may be added to
it); in memory each auth method is its own model
(PasswordAuth, PatAuth, InteractiveOAuthAuth, ClientCredentialsAuth, NoAuth,
UnknownAuth), discriminated on kind and exposed as EnvironmentConfig.auth_config. Each variant
is parsed data declaring the flat keys it owns; two module-level resolvers match on it to produce
credentials — resolve_static_auth for a BfabricAuth held in the file, resolve_credential_provider
for a refreshing OAuthCredentialProvider — so Bfabric.connect() and the auth CLI match on the
variant instead of switching on a string. The two stay apart because static auth has to be
answerable from the environment alone, without a base URL, an environment name or a token-cache read.
The translation is one-way: auth_method_from_flat in config/auth_methods.py is the only place the
flat keys are interpreted, and the variants never serialise themselves back — writers pass the flat
keys to write_environment_to_config directly. Adding an auth method means adding a variant there;
do not reintroduce a parallel list of auth keys elsewhere. EnvironmentConfig keeps auth,
auth_method, client_id, client_secret and scope as read-only properties over the union, so
existing readers are unaffected.
Reading is deliberately tolerant and writing is strict: 1.21.0 wrote environments without any
cross-field validation, so a config already on disk must keep loading even if its keys contradict
each other, while validate_writable_environment refuses to persist a new one that does.
auth_method values this version does not recognise parse to UnknownAuth, which keeps the
unrecognised keys verbatim and raises only when that environment is the one being connected.
For PAT (Personal Access Token) logins the token is stored inline under pat (with
auth_method: pat), never as login: __oauth__ / password: <token>:
PRODUCTION:
base_url: "https://bfabric.example.com/bfabric"
auth_method: "pat"
pat: "<token>"
Backward-compatibility contract. A PAT is not 32 characters, and a ≤1.19.0 client
validates every environment eagerly while enforcing an exactly-32-character password — so an
inline login: __oauth__ / password: <PAT> environment would poison the whole shared
~/.bfabricpy.yml for those clients. Storing the token under pat (no login/password)
means old clients silently ignore it and keep reading the rest of the file; no fleet-wide
upgrade is required. The reader still accepts the legacy login: __oauth__ shape written by
1.20.0rc1.
Generalised, since this is narrower than “the format is frozen” and it is easy to be too cautious:
new config options are free; the envelope is not. app_runner bind-mounts the host’s
~/.bfabricpy.yml into containers whose image may ship an older bfabric, so old readers are live,
not hypothetical — and a version field cannot help, because those readers are already deployed and
will never look for one.
Safe to add:
any new key inside an environment — old readers sweep unknown keys into their client config and ignore them
a whole new environment using a new
auth_method— old readers see it as unauthenticateda new key inside
GENERAL
Breaks every old client on the machine, for the whole file:
a new top-level key (
version:at the root) — anything outsideGENERALis read as an environment name and must parse as onenesting credentials under
auth— that is the old reader’s own field namean environment with no
base_urla
loginpaired with a non-32-characterpassword
tests/bfabric/config/test_backward_compat.py asserts each of these against a vendored replica of
the 1.19.0 schema, so the boundary is executable rather than folklore.
New: config_writer.py#
write_environment_to_config() — creates or updates a YAML environment section with atomic writes (0o600 permissions). Used by all login commands.
ConfigData / EnvironmentConfig#
Both hold the auth-method union described above as auth_config, and expose auth_method, client_id, client_secret and scope as read-only properties derived from it. Bfabric.connect() resolves a credential provider from the method rather than switching on a string: "oauth" yields one backed by the disk token cache, "client_credentials" one backed by the inline secret, and "pat" / "password" yield none, carrying their credential in the config and using the normal auth path.
CLI error handling improvements#
@use_clientdecorator now catchesValueError/RuntimeErrorfromBfabric.connect()and from the wrapped function, printing clean error messages to stderr and exiting with code 1.Removed
@logger.catch(reraise=True)from all API commands (create, read, update, delete, inspect) — error handling is now centralized in@use_client.ResultContainer.assert_success()now formats errors inline ("Query was not successful: Insufficient scope...") instead of passing a tuple.
Dependencies#
authlib— new dependency (added tobfabric/pyproject.toml)httpx— used byregistration.pyfor RFC 7591 HTTP calls
Test coverage#
~1800 lines of new tests across 15 test files covering:
All OAuth flows (PKCE, device code, client credentials, URL token)
Credential provider (token refresh, thread safety, disk caching, cache priority)
Token cache (save/load/clear, path computation, permissions)
Client registration
Webapp client
All 6 CLI auth commands
Config file parsing with new OAuth fields
Config writer
Scope enforcement (server-side)#
B-Fabric now enforces OAuth scopes at the API level:
api:readrequired for SOAP read operationsapi:writerequired for SOAP write operationsAdditional scopes (e.g.
tus,download) can be requested and are enforced by their respective endpoints
The CLI default client is pre-registered with: openid, profile, email, api:read, api:write, tus, download, offline_access.
How to install from this branch#
# As a library dependency
pip install "bfabric @ git+https://github.com/fgcz/bfabricPy.git@oauth-integration#subdirectory=bfabric"
# As a CLI tool (both packages from branch)
uv tool install \
"bfabric-scripts @ git+https://github.com/fgcz/bfabricPy.git@oauth-integration#subdirectory=bfabric_scripts" \
--with "bfabric @ git+https://github.com/fgcz/bfabricPy.git@oauth-integration#subdirectory=bfabric" \
--reinstall