Skip to content

Commit 1bd2a31

Browse files
authored
Merge commit from fork
The client configuration endpoint rejects a registration access token issued to another client. The documentation examples read the client_id from the endpoint URL instead of the request body.
1 parent 311da11 commit 1bd2a31

3 files changed

Lines changed: 82 additions & 11 deletions

File tree

‎authlib/oauth2/rfc7592/endpoint.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def create_configuration_response(self, request):
3939
status_code=401, description="The client does not exist on this server."
4040
)
4141

42+
# The registration access token is only valid for the client it was
43+
# issued to.
44+
if not token.check_client(client):
45+
raise AccessDeniedError()
46+
4247
if not self.check_permission(client, request):
4348
# If the client does not have permission to read its record, the server
4449
# MUST return an HTTP 403 Forbidden.
@@ -146,25 +151,35 @@ def generate_client_registration_info(self, client, request):{
146151
raise NotImplementedError()
147152

148153
def authenticate_token(self, request):
149-
"""Authenticate current credential who is requesting to register a client.
154+
"""Authenticate the registration access token of the request.
150155
Developers MUST implement this method in subclass::
151156
152157
def authenticate_token(self, request):
153158
auth = request.headers.get("Authorization")
154159
return get_token_by_auth(auth)
155160
161+
The token MUST implement
162+
:meth:`~authlib.oauth2.rfc6749.TokenMixin.check_client`. Requests
163+
are denied when the token was not issued to the requested client.
164+
156165
:return: token instance
157166
"""
158167
raise NotImplementedError()
159168

160169
def authenticate_client(self, request):
161-
"""Read a client from the request payload.
170+
"""Read the client from the client configuration endpoint URL.
162171
Developers MUST implement this method in subclass::
163172
173+
from urllib.parse import urlparse
174+
175+
164176
def authenticate_client(self, request):
165-
client_id = request.payload.data.get("client_id")
177+
path = urlparse(request.uri).path
178+
client_id = path.rstrip("/").rsplit("/", 1)[-1]
166179
return Client.get(client_id=client_id)
167180
181+
The route parameter of the web framework can also be used.
182+
168183
:return: client instance
169184
"""
170185
raise NotImplementedError()

‎docs/oauth2/specs/rfc7592.rst‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@ of :ref:`specs/rfc7591`.
1818
Client Configuration Endpoint
1919
-----------------------------
2020

21+
The endpoint URL identifies the client, for instance
22+
``/oauth/register/<client_id>``. The registration access token returned by the
23+
:ref:`specs/rfc7591` registration endpoint is only valid for this client. The
24+
endpoint calls :meth:`~authlib.oauth2.rfc6749.TokenMixin.check_client` on the
25+
token, and denies the request when the token was issued to another client.
26+
2127
Before register the endpoint, developers MUST implement the missing methods::
2228

29+
from urllib.parse import urlparse
30+
2331
from authlib.oauth2.rfc7592 import ClientConfigurationEndpoint
2432

2533

@@ -33,7 +41,11 @@ Before register the endpoint, developers MUST implement the missing methods::
3341
return token
3442

3543
def authenticate_client(self, request):
36-
client_id = request.payload.data.get('client_id')
44+
# read the client_id from the endpoint URL, not from the body:
45+
# GET and DELETE requests have no body.
46+
# The route parameter of the web framework can also be used.
47+
path = urlparse(request.uri).path
48+
client_id = path.rstrip('/').rsplit('/', 1)[-1]
3749
return Client.get(client_id=client_id)
3850

3951
def revoke_access_token(self, token, request):
@@ -46,13 +58,8 @@ Before register the endpoint, developers MUST implement the missing methods::
4658
def delete_client(self, client, request):
4759
client.delete()
4860

49-
def save_client(self, client_info, client_metadata, request):
50-
client = OAuthClient(
51-
user_id=request.credential.user_id,
52-
client_id=client_info['client_id'],
53-
client_secret=client_info['client_secret'],
54-
**client_metadata,
55-
)
61+
def update_client(self, client, client_metadata, request):
62+
client.set_client_metadata(client_metadata)
5663
client.save()
5764
return client
5865

‎tests/flask/test_oauth2/test_client_configuration_endpoint.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ def test_read_unauthorized_client(test_client, token):
141141
client_secret="unauthorized_client_secret",
142142
)
143143
db.session.add(client)
144+
token.client_id = "unauthorized_client_id"
145+
db.session.add(token)
144146

145147
headers = {"Authorization": f"bearer {token.access_token}"}
146148
rv = test_client.get("/configure_client/unauthorized_client_id", headers=headers)
@@ -267,6 +269,8 @@ def test_update_unauthorized_client(test_client, token):
267269
client_secret="unauthorized_client_secret",
268270
)
269271
db.session.add(client)
272+
token.client_id = "unauthorized_client_id"
273+
db.session.add(token)
270274

271275
headers = {"Authorization": f"bearer {token.access_token}"}
272276
rv = test_client.put(
@@ -487,9 +491,54 @@ def test_delete_unauthorized_client(test_client, token):
487491
client_secret="unauthorized_client_secret",
488492
)
489493
db.session.add(client)
494+
token.client_id = "unauthorized_client_id"
495+
db.session.add(token)
490496

491497
headers = {"Authorization": f"bearer {token.access_token}"}
492498
rv = test_client.delete("/configure_client/unauthorized_client_id", headers=headers)
493499
resp = json.loads(rv.data)
494500
assert rv.status_code == 403
495501
assert resp["error"] == "unauthorized_client"
502+
503+
504+
@pytest.fixture
505+
def other_client(db):
506+
client = Client(client_id="other-client-id", client_secret="other-client-secret")
507+
client.set_client_metadata({"client_name": "Other"})
508+
db.session.add(client)
509+
db.session.commit()
510+
yield client
511+
db.session.delete(client)
512+
513+
514+
def test_read_token_of_another_client(test_client, other_client, token):
515+
"""A registration access token cannot read another client."""
516+
headers = {"Authorization": f"bearer {token.access_token}"}
517+
rv = test_client.get("/configure_client/other-client-id", headers=headers)
518+
resp = json.loads(rv.data)
519+
assert rv.status_code == 400
520+
assert resp["error"] == "access_denied"
521+
assert "client_secret" not in resp
522+
523+
524+
def test_update_token_of_another_client(test_client, other_client, token):
525+
"""A registration access token cannot update another client."""
526+
headers = {"Authorization": f"bearer {token.access_token}"}
527+
body = {"client_id": "other-client-id", "client_name": "Hijacked"}
528+
rv = test_client.put(
529+
"/configure_client/other-client-id", json=body, headers=headers
530+
)
531+
resp = json.loads(rv.data)
532+
assert rv.status_code == 400
533+
assert resp["error"] == "access_denied"
534+
assert other_client.client_name == "Other"
535+
536+
537+
def test_delete_token_of_another_client(test_client, other_client, token):
538+
"""A registration access token cannot delete another client."""
539+
headers = {"Authorization": f"bearer {token.access_token}"}
540+
rv = test_client.delete("/configure_client/other-client-id", headers=headers)
541+
resp = json.loads(rv.data)
542+
assert rv.status_code == 400
543+
assert resp["error"] == "access_denied"
544+
assert Client.query.filter_by(client_id="other-client-id").first()

0 commit comments

Comments
 (0)