Skip to content

feat(auth): make bearer-token resolution pluggable via AuthBackend protocol - #732

Closed
xiejava1018 wants to merge 1 commit into
AgentFlocks:mainfrom
xiejava1018:upstream/pluggable-bearer-auth
Closed

feat(auth): make bearer-token resolution pluggable via AuthBackend protocol#732
xiejava1018 wants to merge 1 commit into
AgentFlocks:mainfrom
xiejava1018:upstream/pluggable-bearer-auth

Conversation

@xiejava1018

Copy link
Copy Markdown

Motivation

Today, non-browser requests to Flocks authenticate via a single shared API
token stored in .secret.json. Leaking that token grants admin-equivalent
access across all tenants — which is incompatible with multi-tenant
service-to-service deployments (e.g. an upstream facade that wants to call
Flocks with per-team identities).

Proposal

Extend the existing AuthBackend protocol with two optional methods:

@classmethod
async def supports_bearer_token(cls) -> bool: ...           # default False

@classmethod
async def authenticate_bearer_token(
    cls,
    token: str,
    *,
    audience: Optional[str] = None,
) -> Optional["LocalUser"]: ...                            # default None

Backends that override these opt in to serving Bearer auth themselves.
Default behavior is unchanged — backends that don't implement the new
methods keep falling through to the existing API-token check.

Why this shape

  • Zero break for existing deployments: backends that don't opt in keep
    working with the shared API token, no env-var churn.
  • No new configuration plumbing — opt-in is via the backend itself,
    which is already selected at startup.
  • Protocol-compatible: any backend implementing both methods serves
    cookie + bearer auth symmetrically.
  • No new dependencies: pyjwt is already a transitive dependency; the
    contract is just "give me a token, I give you a user".

Use case: multi-tenant facade

A facade issues a short-lived JWT per (user, team) request:

{ sub, username, teams, permissions, iss, aud, exp }

The Flocks deployment installs a backend (e.g. JWTAuthBackend) that
implements authenticate_bearer_token() to decode + verify the JWT (JWKS
fetch with cache) and return a LocalUser whose to_auth_user().tenant_ids
carries the team scope. The facade then calls Flocks with
Authorization: Bearer <jwt> and gets real per-team isolation inside
Flocks — no shared API token involved.

Rollout plan

  1. This PR — protocol extension + tests. No server-side change yet.
  2. Follow-up PR — server-side apply_auth_for_request consults
    supports_bearer_token() and routes accordingly. Cookie path
    completely untouched.
  3. Sample backend PR (separate) — reference implementation of
    JWTAuthBackend for the use case above.

Migration

None required. All backends continue to behave as today unless they
explicitly opt in.

PoC / reference

A working PoC that demonstrates this protocol extension in a multi-tenant
facade scenario lives on the workshop/poc-c0 branch of
github.com/xiejava1018/flocks. It includes:

  • flocks/workshop_auth/ — a JWT-based backend that implements
    authenticate_bearer_token()
  • tests/poc_workshop_auth.py — end-to-end verification using real
    _apply_auth_for_request
  • tests/poc_tenant_isolation.py — verifies tenant_ids reaches Flocks'
    policy layer (flocks/contracts/access)

The PoC is intentionally not part of this PR — it depends on
Workshop facade specifics that don't belong in upstream Flocks. Happy to
extract a generic JWTAuthBackend as a follow-up if reviewers want.

Files changed

  • flocks/auth/backend.py — two optional methods added (34 lines)
  • tests/test_auth_backend_bearer_protocol.py — defaults + opt-in tests

Open questions

  • Should the new methods live on AuthBackend or on a separate
    BearerAuthBackend mixin? Leaning toward AuthBackend for simplicity —
    the cookie/bearer symmetry is intentional.
  • Should the follow-up server-side PR also handle a "register adapter
    pattern" so third-party backends can extend extension_context without
    forking? Out of scope here, but worth a separate discussion.

…otocol

Today, non-browser requests to Flocks authenticate via a single shared
API token stored in .secret.json. Leaking it grants admin-equivalent
access across all tenants — incompatible with multi-tenant service-to-
service deployments (e.g. a facade issuing per-team JWTs).

Extend the existing AuthBackend protocol with two OPTIONAL methods:

  - supports_bearer_token()           default False
  - authenticate_bearer_token(token) default None

Backends that override these opt in to serving Bearer auth themselves.
The follow-up server-side PR will consult supports_bearer_token() in
apply_auth_for_request and route accordingly; until then this PR is a
pure protocol extension with zero behavior change.

Use case: a JWTAuthBackend (sample in a separate PR) decodes a
short-lived per-team JWT, returns LocalUser with tenant_ids populated,
and gives the facade real per-team isolation in Flocks without sharing
the API token across tenants.

Refs:
- PoC reference implementation (not part of this PR):
  github.com/xiejava1018/flocks @ workshop/poc-c0
- Design: see PR description
xiejava1018 pushed a commit to xiejava1018/flocks that referenced this pull request Sep 3, 2026
uvicorn 全局异常处理器把 pyjwt 异常包成 500 '鉴权处理异常', 破坏客户端
重试语义。框架契约: get_user_by_session_id → None ⇒ 401 '登录已过期'。

真实服务联调验证(FLOCKS_AUTH=workshop_jwt, uvicorn flocks.server.app:app):
- 伪造 Cookie JWT → 401 登录已过期(修复前 500)
- 合法 team JWT → GET/POST /api/session 200(真实会话 ses_* 创建成功)
- Bearer-only → 401(设计事实: 不走 backend, 印证上游 PR AgentFlocks#732 动机)
- env-gated 挂载在真实启动链路生效(TeamJWTAuthBackend registered)

单测同步: forged/expired 两用例改断言 None; 30/30 全绿。
另修复测试 docstring 全角引号导致的语法错误。
@xiejava1018

Copy link
Copy Markdown
Author

关闭此 PR:误提到上游 AgentFlocks/flocks。已改在 fork 仓 xiejava1018/flocks 内部评审合并(workshop_auth 插件属于 Workshop 私有集成, 不适合直接进上游)。

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