Skip to content

Commit fce9921

Browse files
authored
fix: allow unauthenticated oauth client registration (#109)
1 parent 1b20e17 commit fce9921

File tree

5 files changed

+127
-91
lines changed

5 files changed

+127
-91
lines changed

packages/belgie-alchemy/src/belgie_alchemy/__tests__/integration/core/oauth/test_routes_register.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,11 @@ async def test_register_enabled_unauthenticated_allows_omitted_auth_method(
134134

135135

136136
@pytest.mark.asyncio
137-
async def test_register_enabled_unauthenticated_rejects_explicit_confidential_clients(
137+
@pytest.mark.parametrize("auth_method", ["client_secret_post", "client_secret_basic"])
138+
async def test_register_enabled_unauthenticated_allows_explicit_confidential_clients(
138139
belgie_instance,
139140
oauth_settings: OAuthServer,
141+
auth_method: str,
140142
) -> None:
141143
settings_payload = oauth_settings.model_dump(mode="python")
142144
settings_payload["allow_dynamic_client_registration"] = True
@@ -151,13 +153,42 @@ async def test_register_enabled_unauthenticated_rejects_explicit_confidential_cl
151153
"/auth/oauth/register",
152154
json={
153155
"redirect_uris": ["http://testserver/callback"],
154-
"token_endpoint_auth_method": "client_secret_post",
156+
"token_endpoint_auth_method": auth_method,
157+
},
158+
)
159+
160+
assert response.status_code == 200
161+
payload = response.json()
162+
assert payload["client_secret"] is not None
163+
assert payload["token_endpoint_auth_method"] == auth_method
164+
165+
166+
@pytest.mark.asyncio
167+
@pytest.mark.parametrize("auth_method", ["client_secret_post", "client_secret_basic"])
168+
async def test_register_enabled_without_unauthenticated_registration_rejects_confidential_clients(
169+
belgie_instance,
170+
oauth_settings: OAuthServer,
171+
auth_method: str,
172+
) -> None:
173+
settings_payload = oauth_settings.model_dump(mode="python")
174+
settings_payload["allow_dynamic_client_registration"] = True
175+
settings = OAuthServer(**settings_payload)
176+
belgie_instance.add_plugin(settings)
177+
app = FastAPI()
178+
app.include_router(belgie_instance.router)
179+
transport = httpx.ASGITransport(app=app)
180+
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
181+
response = await client.post(
182+
"/auth/oauth/register",
183+
json={
184+
"redirect_uris": ["http://testserver/callback"],
185+
"token_endpoint_auth_method": auth_method,
155186
},
156187
)
157188

158189
assert response.status_code == 401
159190
payload = response.json()
160-
assert payload["error"] == "invalid_request"
191+
assert payload["error"] == "invalid_token"
161192

162193

163194
@pytest.mark.asyncio

packages/belgie-alchemy/src/belgie_alchemy/__tests__/unit/core/mixins/test_mixins.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ def test_primary_key_mixin_defaults() -> None:
1111
id_column = User.__table__.c.id # type: ignore[attr-defined]
1212
assert id_column.primary_key
1313
assert str(id_column.server_default.arg) == "gen_random_uuid()"
14-
assert id_column.index
1514

1615

1716
def test_primary_key_client_side_generation() -> None:

packages/belgie-oauth-server/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ To migrate existing clients:
2020
- Configure a resource with `resources=[OAuthResource(...)]`.
2121
- Or stop sending the `resource` parameter.
2222

23+
## Dynamic Client Registration
24+
25+
If `allow_dynamic_client_registration=True`, Belgie serves `POST /auth/oauth/register` for OAuth Dynamic Client
26+
Registration.
27+
28+
If `allow_unauthenticated_client_registration=True`, anonymous registration is allowed for both:
29+
30+
- public clients (`token_endpoint_auth_method="none"`)
31+
- confidential clients (`client_secret_post`, `client_secret_basic`, or omitted auth method)
32+
33+
When the auth method is omitted, Belgie preserves provider-side defaulting and registers the client as
34+
`client_secret_post`.
35+
36+
This setting is intentionally permissive. Any anonymous caller can register a confidential client and receive a client
37+
secret, so treat it as a development or compatibility escape hatch unless you have separate controls around DCR.
38+
2339
## ID Token Signing for Public Clients
2440

2541
`id_token` signing and verification use the client secret-derived key for confidential clients. Public clients (with

packages/belgie-oauth-server/src/belgie_oauth_server/plugin.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -333,13 +333,13 @@ async def token_handler(
333333
return router
334334

335335
@staticmethod
336-
def _add_register_route( # noqa: C901
336+
def _add_register_route(
337337
router: APIRouter,
338338
belgie: Belgie,
339339
provider: SimpleOAuthProvider,
340340
settings: OAuthServer,
341341
) -> APIRouter:
342-
async def register_handler( # noqa: PLR0911
342+
async def register_handler(
343343
request: Request,
344344
client: Annotated[BelgieClient, Depends(belgie)],
345345
) -> Response:
@@ -371,22 +371,12 @@ async def register_handler( # noqa: PLR0911
371371
if exc.status_code != status.HTTP_401_UNAUTHORIZED:
372372
raise
373373

374-
# Treat omitted auth method like public registration here so MCP clients can
375-
# register anonymously; the provider still defaults it later.
376-
is_public_client = metadata.token_endpoint_auth_method in {None, "none"}
377-
if not authenticated:
378-
if not settings.allow_unauthenticated_client_registration:
379-
return _oauth_error(
380-
"invalid_token",
381-
"authentication required for client registration",
382-
status_code=401,
383-
)
384-
if not is_public_client:
385-
return _oauth_error(
386-
"invalid_request",
387-
"authentication required for confidential client registration",
388-
status_code=401,
389-
)
374+
if not authenticated and not settings.allow_unauthenticated_client_registration:
375+
return _oauth_error(
376+
"invalid_token",
377+
"authentication required for client registration",
378+
status_code=401,
379+
)
390380

391381
try:
392382
client_info = await provider.register_client(metadata)

0 commit comments

Comments
 (0)