Skip to content

Experimental OAuth credential path for bfabric_asgi_auth + rest_proxy service-user migration#513

Draft
leoschwarz wants to merge 3 commits into
mainfrom
worktree-greedy-splashing-shore
Draft

Experimental OAuth credential path for bfabric_asgi_auth + rest_proxy service-user migration#513
leoschwarz wants to merge 3 commits into
mainfrom
worktree-greedy-splashing-shore

Conversation

@leoschwarz

Copy link
Copy Markdown
Member

Summary

  • Adds an experimental OAuth credential path to bfabric_asgi_auth alongside the existing legacy path — all legacy BDD tests continue to pass unchanged
  • Migrates bfabric_rest_proxy service account from legacy feeder-user (webservice password) to OAuth client_credentials grant
  • Introduces WebappOAuthSettings + OAuthClientCredentials in bfabric.experimental to supersede the deprecated WebappIntegrationSettings

bfabric core (experimental/)

  • New: webapp_oauth_settings.pyOAuthClientCredentials(client_id, client_secret, scope) and WebappOAuthSettings(base_url, credentials); these supersede WebappIntegrationSettings for OAuth deployments
  • Deprecated: webapp_integration_settings.py now emits DeprecationWarning at import; retained until prod OAuth cutover

bfabric_asgi_auth

  • New create_oauth_validator(settings) — exchanges a B-Fabric launch JWT for access+refresh tokens via RFC 8693 (exchange_token) and verifies claims locally against JWKS (verify_jwt)
  • New OAuthExchangeSuccess / OAuthSessionData / BfabricOAuthUser — parallel data model to the legacy trio; client_secret is never stored in the cookie session
  • New make_oauth_session_factory / make_oauth_user_factory — pluggable factories; injected via new session_factory / user_factory kwargs on BfabricAuthMiddleware (defaults preserve legacy)
  • New create_mock_oauth_validator + OAuthMockFixture for testing without a live server
  • New BDD feature oauth_authentication.feature (6 scenarios)
  • New design doc docs/design/oauth_migration.md — three-layer model, per-user token storage constraint, starlette-authlib verdict, migration timeline
  • Fixed token_context.feature was unregistered; unit tests were excluded from testpaths

bfabric_rest_proxy

  • ServerSettings.service_user_credentials: dict[str, OAuthClientCredentials] replaces legacy feeder_user_credentials
  • get_bfabric_service_client (renamed from get_bfabric_feeder_client) builds the privileged client via Bfabric.connect_oauth with shared disk cache (correct for service identity)
  • /validate_token returns 501 when validation_bfabric_instance is not configured
  • User-facing legacy auth (BfabricAuthParam / /read) deferred to prod OAuth cutover

Key design constraint

Per-user OAuth tokens are stored in the encrypted cookie session (OAuthSessionData.token) and OAuthCredentialProvider is always constructed with token_cache_path=None — the shared disk cache keyed by (base_url, client_id) cannot distinguish users. Known limitation: refreshed tokens are not written back to the cookie mid-request (documented in the migration doc as future work).

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

📝 "TODO" Changes Detected

Summary: ➕ 3 "TODO"s added

➕ Added "TODO"s (3)

  • bfabric/src/bfabric/experimental/webapp_oauth.py:49: # TODO(confirm aud): wire audience=client_id once the server's real aud claim is confirmed.
  • bfabric/src/bfabric/experimental/webapp_oauth.py:50: # TODO(confirm iss): wire issuer once confirmed via {base_url}/rest/oauth/.well-known/openid-configuration.
  • bfabric/docs/design/oauth_integration.md:150: validation** (active # TODO(confirm aud/iss) comments). All tests use synthetic RSA keys and mocked

This comment is automatically updated when "TODO" changes are detected.

…513)

Replaces the parallel OAuth stack in bfabric_asgi_auth with a thin layer
over new public primitives in bfabric.experimental, fixes four confirmed
defects, and eliminates all private cross-package imports.

Core (bfabric):
- Add `bfabric.experimental.webapp_oauth`: public entry point exposing
  `exchange_launch_token`, `UrlTokenContext`, `DEFAULT_OAUTH_SCOPE`
- Add `bfabric.experimental.webapp_oauth_settings`: `WebappOAuthSettings`
  and `OAuthClientCredentials` config models
- Add `Bfabric.connect_oauth_token`: single refresh-token-grant builder
  shared by WebappClient, connect_pkce, connect_device_code, and the ASGI
  per-request rebuild
- Add `on_token_update` callback hook to `OAuthCredentialProvider`; fires
  after each refresh, dropped on pickle (correct — cookie is source of truth)
- Add opt-in `audience`/`issuer` params to `verify_jwt` (default None,
  no behaviour change until server claims are confirmed)

ASGI (bfabric_asgi_auth):
- Add `session_factory`/`user_factory` seam to `BfabricAuthMiddleware`;
  defaults reproduce legacy SOAP path for backward compatibility
- Add `BfabricOAuthUser` with entity-context properties and refresh
  write-back via `_on_token_refresh` into the live Starlette session dict
- Add `OAuthSessionData`: minimal (base_url, token, context) cookie payload
- Add `OAuthExchangeSuccess` discriminated-union member and
  `create_oauth_validator` factory
- Fix mock `job_id` determinism: `abs(hash(username))` → `zlib.crc32`
- Zero private `bfabric._oauth.*` imports remain in bfabric_asgi_auth/src

Tests: 716 passing; 0 basedpyright errors/warnings on both packages
@leoschwarz leoschwarz force-pushed the worktree-greedy-splashing-shore branch from db3cf68 to e3489df Compare June 11, 2026 07:27
Delete BfabricUser, SessionData, create_bfabric_validator, create_mock_validator,
TokenValidationSuccess, and the session_factory/user_factory middleware seam.
BfabricAuthMiddleware now takes explicit client_id/client_secret and builds
BfabricOAuthUser directly, threading the live session dict so refresh tokens
are written back to the encrypted cookie automatically.

AuthHooks.on_success payload changes from TokenData to UrlTokenContext.
Eviction deferred to a follow-up (behaviour note in changelog).
BDD suite repointed to the new mock OAuth validator.
The suppress comment was on the closing `)` line rather than the line
containing the expression (`dict(self._session.token)`), causing four
basedpyright warnings (two real + two "unnecessary ignore").  Move the
comment to the dict(...) line, matching the identical pattern in _persist.
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.

1 participant