Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions flocks/auth/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,37 @@ async def reassign_orphan_sessions(
@classmethod
async def migrate_legacy_sessions_to_admin(cls, admin_user_id: str) -> None: ...

# ----- Optional: bearer / machine-to-machine auth (proposed) -----
#
# These two methods are OPTIONAL on the protocol. Backends that do not
# implement them simply return ``False`` / ``None`` respectively, and the
# server-side auth middleware falls back to the existing API-token path.
#
# Backends that DO implement them (e.g. a JWT / OIDC backend) take over
# Bearer token authentication for non-browser, non-cookie requests.
# This unlocks service-to-service use cases without leaking the shared
# API token across deployments.

@classmethod
async def supports_bearer_token(cls) -> bool:
"""Whether this backend can authenticate non-cookie Bearer tokens.

Default: ``False``. Backends should override to opt in.
"""
return False

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

Default: ``None``. Backends that override ``supports_bearer_token``
to ``True`` must also override this to return the LocalUser whose
claims match the token, or ``None`` to indicate invalid/untrusted.
"""
return None

52 changes: 52 additions & 0 deletions tests/test_auth_backend_bearer_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tests for the optional bearer-token AuthBackend protocol methods.

These methods default to "no opt-in" so existing backends keep working.
"""

from __future__ import annotations

import pytest


class _DefaultBackend:
"""Mirrors the default implementation of the new optional methods.

Doesn't subclass anything — just verifies that callers can rely on the
defaults returning False / None.
"""

@classmethod
async def supports_bearer_token(cls) -> bool:
return False

@classmethod
async def authenticate_bearer_token(cls, token: str, *, audience=None):
return None


@pytest.mark.asyncio
async def test_default_supports_bearer_token_is_false():
assert await _DefaultBackend.supports_bearer_token() is False


@pytest.mark.asyncio
async def test_default_authenticate_bearer_token_is_none():
assert await _DefaultBackend.authenticate_bearer_token("anything") is None


@pytest.mark.asyncio
async def test_opt_in_backend_overrides():
"""A backend that opts in returns True and resolves the token."""

class _OptInBackend:
@classmethod
async def supports_bearer_token(cls) -> bool:
return True

@classmethod
async def authenticate_bearer_token(cls, token: str, *, audience=None):
return f"local-user-for-{token}-aud-{audience}"

assert await _OptInBackend.supports_bearer_token() is True
user = await _OptInBackend.authenticate_bearer_token("xyz", audience="flocks")
assert user == "local-user-for-xyz-aud-flocks"