Skip to content

feat(bfabric): expose the authorization-code primitives for confidential clients - #628

Draft
leoschwarz wants to merge 5 commits into
mainfrom
feature/oauth-authorization-code-primitives
Draft

feat(bfabric): expose the authorization-code primitives for confidential clients#628
leoschwarz wants to merge 5 commits into
mainfrom
feature/oauth-authorization-code-primitives

Conversation

@leoschwarz

@leoschwarz leoschwarz commented Sep 1, 2026

Copy link
Copy Markdown
Member
  • Add bfabric.oauth.AuthorizationRequest.create, which starts an authorization-code login: it returns the URL to redirect a user to, plus the CSRF state and PKCE verifier to keep until they come back. A web app no longer derives a code challenge itself, so it cannot produce one that disagrees with its verifier.
  • Add bfabric.oauth.exchange_code to redeem the returned code for tokens, with an optional client_secret sent as client_secret_basic for confidential clients. Omitting it makes the request as a public client, relying on PKCE alone.
  • Add bfabric.oauth.token_url, which builds the token endpoint URL for an instance.
  • Not yet confirmed that B-Fabric accepts client_secret_basic on the authorization_code grant specifically: Basic is proven for the RFC 8693 exchange, and the POST-body form is proven for authorization_code. Needs one live login against a real instance before this merges.

…ial clients

A web app driving its own authorization-code login cannot use pkce_login: its
callback lands on a registered public redirect URI, not the loopback listener
pkce_login starts. Everything upstream of that listener is reusable, so make the
final leg public rather than have each app reimplement it.

Only the parts that can actually drift are exported. The authorize parameters
(RFC 6749 4.1.1, RFC 7636 4.3) and the token request body (4.1.3, 4.5) are fixed
by spec and are left for callers to build; what is B-Fabric's own are the two
endpoint paths, and what no spec pins is which client authentication method the
token endpoint accepts, so client_secret follows the client_secret_basic form
already used for the RFC 8693 exchange.

An empty client_secret is treated as absent, because that is how the rest of the
library spells "public client".

The token path was built at eight call sites; four of them are in bfabric.py and
_webapp_client.py, which #621 rewrites, so those are left for a later sweep.
…dpoint

authorize_url returned the bare endpoint, leaving every caller to append the
same seven query parameters. Those parameters are fixed by RFC 6749 4.1.1 and
RFC 7636 4.3, so no copy of that block can drift -- but two copies had already
appeared, and exporting the token exchange while leaving half the redirect leg
to the caller is an odd surface for a library that claims to offer the flow.

Fold the query into authorize_url rather than adding a second function beside
it: once the full builder exists the bare endpoint has no caller left, so this
keeps the exported surface at three names and avoids a confusing near-synonym.
authorize_url asked the caller for a code_challenge and a state, both of which
pkce_login generates three lines above the call. So the exported half of the
flow handed back the hard part: a consumer had to implement RFC 7636 4.1 and
4.2 and base64url-without-padding to use functions whose purpose is hiding
exactly that, and a mistake surfaces only at the token step, one redirect
later, as an opaque server rejection.

AuthorizationRequest.create generates the verifier, derives the challenge and
picks the CSRF state, returning the redirect URL plus the two values the caller
has to carry across the round trip. Nothing PKCE-shaped is left in the public
vocabulary and the challenge cannot disagree with the verifier.

authorize_url stops being exported, since the arguments that made it public are
the ones now generated. token_url stays: OAuthCredentialProvider takes a
token_url, so an external caller building one still needs it. Giving the
provider a base_url instead would change the field it pickles, so that is left
alone.

Also export verify_jwt, already used by WebappClient and already caching JWKS
for an hour, so a caller stops hand-rolling base64url to read a token's claims.
basedpyright runs against a baseline that grandfathers the existing
reportUnannotatedClassAttribute warnings, so a new one fails CI even though
45 like it already exist. Annotate with the ClassVar[ConfigDict] that pydantic
itself declares, which satisfies the rule without the reportIncompatible-
VariableOverride an unparameterised ConfigDict annotation would provoke.
It was exported on the strength of a consumer that then turned out not to
want it: the log-viewer webapp learns its instance from the token's own
iss, so it cannot name a base_url before decoding, and verify_jwt takes
one. With no external caller left, exporting it only fixes a name we would
have to keep.

Nothing else changes — WebappClient still imports it from _url_token, so
it stays covered by tests. Re-exporting is one line in __all__ if a webapp
serving a single configured instance ever shows up; at that point it should
also take a str and normalise, like its exported siblings do.
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