Skip to content
This repository was archived by the owner on Jun 23, 2023. It is now read-only.

Commit 155ef33

Browse files
authored
Merge pull request #85 from nsklikas/feature-refresh-scopes
Allow requesting for scopes in refresh
2 parents dc4be5e + b6b8658 commit 155ef33

4 files changed

Lines changed: 406 additions & 18 deletions

File tree

src/oidcop/oauth2/token.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def _mint_token(
5353
session_id: str,
5454
client_id: str,
5555
based_on: Optional[SessionToken] = None,
56+
scope: Optional[list] = None,
5657
token_args: Optional[dict] = None,
5758
token_type: Optional[str] = ""
5859
) -> SessionToken:
@@ -80,6 +81,7 @@ def _mint_token(
8081
token_handler=_mngr.token_handler[token_class],
8182
based_on=based_on,
8283
usage_rules=usage_rules,
84+
scope=scope,
8385
token_type=token_type,
8486
**_args,
8587
)
@@ -136,7 +138,6 @@ def process_request(self, req: Union[Message, dict], **kwargs):
136138
_log_debug("All checks OK")
137139

138140
issue_refresh = kwargs.get("issue_refresh", False)
139-
140141
_response = {
141142
"token_type": "Bearer",
142143
"scope": grant.scope,
@@ -225,15 +226,29 @@ def process_request(self, req: Union[Message, dict], **kwargs):
225226

226227
token_value = req["refresh_token"]
227228
_session_info = _mngr.get_session_info_by_token(token_value, grant=True)
228-
229229
_grant = _session_info["grant"]
230+
231+
token_type = "Bearer"
232+
233+
# Is DPOP supported
234+
if "dpop_signing_alg_values_supported" in _context.provider_info:
235+
_dpop_jkt = req.get("dpop_jkt")
236+
if _dpop_jkt:
237+
_grant.extra["dpop_jkt"] = _dpop_jkt
238+
token_type = "DPoP"
239+
230240
token = _grant.get_token(token_value)
241+
scope = _grant.find_scope(token.based_on)
242+
if "scope" in req:
243+
scope = req["scope"]
231244
access_token = self._mint_token(
232245
token_class="access_token",
233246
grant=_grant,
234247
session_id=_session_info["session_id"],
235248
client_id=_session_info["client_id"],
236249
based_on=token,
250+
scope=scope,
251+
token_type=token_type,
237252
)
238253

239254
_resp = {
@@ -246,13 +261,15 @@ def process_request(self, req: Union[Message, dict], **kwargs):
246261
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
247262

248263
_mints = token.usage_rules.get("supports_minting")
249-
if "refresh_token" in _mints:
264+
issue_refresh = kwargs.get("issue_refresh", False)
265+
if "refresh_token" in _mints and issue_refresh:
250266
refresh_token = self._mint_token(
251267
token_class="refresh_token",
252268
grant=_grant,
253269
session_id=_session_info["session_id"],
254270
client_id=_session_info["client_id"],
255271
based_on=token,
272+
scope=scope,
256273
)
257274
refresh_token.usage_rules = token.usage_rules.copy()
258275
_resp["refresh_token"] = refresh_token.value
@@ -288,7 +305,8 @@ def post_parse_request(
288305
logger.error("Access Code invalid")
289306
return self.error_cls(error="invalid_grant")
290307

291-
token = _session_info["grant"].get_token(request["refresh_token"])
308+
grant = _session_info["grant"]
309+
token = grant.get_token(request["refresh_token"])
292310

293311
if not isinstance(token, RefreshToken):
294312
return self.error_cls(error="invalid_request", error_description="Wrong token type")
@@ -298,6 +316,15 @@ def post_parse_request(
298316
error="invalid_request", error_description="Refresh token inactive"
299317
)
300318

319+
if "scope" in request:
320+
req_scopes = set(request["scope"])
321+
scopes = set(grant.find_scope(token.based_on))
322+
if scopes < req_scopes:
323+
return self.error_cls(
324+
error="invalid_request",
325+
error_description="Invalid refresh scopes",
326+
)
327+
301328
return request
302329

303330

src/oidcop/oidc/token.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,17 @@ def process_request(self, req: Union[Message, dict], **kwargs):
202202
token_type = "DPoP"
203203

204204
token = _grant.get_token(token_value)
205+
scope = _grant.find_scope(token.based_on)
206+
if "scope" in req:
207+
scope = req["scope"]
205208
access_token = self._mint_token(
206209
token_class="access_token",
207210
grant=_grant,
208211
session_id=_session_info["session_id"],
209212
client_id=_session_info["client_id"],
210213
based_on=token,
211-
token_type=token_type
214+
scope=scope,
215+
token_type=token_type,
212216
)
213217

214218
_resp = {
@@ -221,25 +225,33 @@ def process_request(self, req: Union[Message, dict], **kwargs):
221225
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
222226

223227
_mints = token.usage_rules.get("supports_minting")
224-
if "refresh_token" in _mints:
228+
issue_refresh = False
229+
if "issue_refresh" in kwargs:
230+
issue_refresh = kwargs["issue_refresh"]
231+
else:
232+
if "offline_access" in scope:
233+
issue_refresh = True
234+
if "refresh_token" in _mints and issue_refresh:
225235
refresh_token = self._mint_token(
226236
token_class="refresh_token",
227237
grant=_grant,
228238
session_id=_session_info["session_id"],
229239
client_id=_session_info["client_id"],
230240
based_on=token,
241+
scope=scope,
231242
)
232243
refresh_token.usage_rules = token.usage_rules.copy()
233244
_resp["refresh_token"] = refresh_token.value
234245

235-
if "id_token" in _mints:
246+
if "id_token" in _mints and "openid" in scope:
236247
try:
237248
_idtoken = self._mint_token(
238249
token_class="refresh_token",
239250
grant=_grant,
240251
session_id=_session_info["session_id"],
241252
client_id=_session_info["client_id"],
242253
based_on=token,
254+
scope=scope,
243255
)
244256
except (JWEException, NoSuitableSigningKeys) as err:
245257
logger.warning(str(err))
@@ -281,7 +293,8 @@ def post_parse_request(
281293
logger.error("Access Code invalid")
282294
return self.error_cls(error="invalid_grant")
283295

284-
token = _session_info["grant"].get_token(request["refresh_token"])
296+
grant = _session_info["grant"]
297+
token = grant.get_token(request["refresh_token"])
285298

286299
if not isinstance(token, RefreshToken):
287300
return self.error_cls(error="invalid_request", error_description="Wrong token type")
@@ -291,6 +304,15 @@ def post_parse_request(
291304
error="invalid_request", error_description="Refresh token inactive"
292305
)
293306

307+
if "scope" in request:
308+
req_scopes = set(request["scope"])
309+
scopes = set(grant.find_scope(token.based_on))
310+
if scopes < req_scopes:
311+
return self.error_cls(
312+
error="invalid_request",
313+
error_description="Invalid refresh scopes",
314+
)
315+
294316
return request
295317

296318

tests/test_24_oauth2_token_endpoint.py

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
AUTH_REQ = AuthorizationRequest(
4848
client_id="client_1",
4949
redirect_uri="https://example.com/cb",
50-
scope=["openid"],
50+
scope=["email"],
5151
state="STATE",
5252
response_type="code",
5353
)
@@ -302,7 +302,7 @@ def test_process_request_using_private_key_jwt(self):
302302

303303
def test_do_refresh_access_token(self):
304304
areq = AUTH_REQ.copy()
305-
areq["scope"] = ["openid"]
305+
areq["scope"] = ["email"]
306306

307307
session_id = self._create_session(areq)
308308
grant = self.endpoint_context.authz(session_id, areq)
@@ -324,7 +324,7 @@ def test_do_refresh_access_token(self):
324324
_token.usage_rules["supports_minting"] = ["access_token", "refresh_token"]
325325

326326
_req = self.token_endpoint.parse_request(_request.to_json())
327-
_resp = self.token_endpoint.process_request(request=_req)
327+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
328328
assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"}
329329
assert set(_resp["response_args"].keys()) == {
330330
"access_token",
@@ -338,7 +338,7 @@ def test_do_refresh_access_token(self):
338338

339339
def test_do_2nd_refresh_access_token(self):
340340
areq = AUTH_REQ.copy()
341-
areq["scope"] = ["openid", "offline_access"]
341+
areq["scope"] = ["email"]
342342

343343
session_id = self._create_session(areq)
344344
grant = self.endpoint_context.authz(session_id, areq)
@@ -361,16 +361,15 @@ def test_do_2nd_refresh_access_token(self):
361361
_token.usage_rules["supports_minting"] = [
362362
"access_token",
363363
"refresh_token",
364-
"id_token",
365364
]
366365

367366
_req = self.token_endpoint.parse_request(_request.to_json())
368-
_resp = self.token_endpoint.process_request(request=_req)
367+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
369368

370369
_2nd_request = REFRESH_TOKEN_REQ.copy()
371370
_2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"]
372371
_2nd_req = self.token_endpoint.parse_request(_request.to_json())
373-
_2nd_resp = self.token_endpoint.process_request(request=_req)
372+
_2nd_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
374373

375374
assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"}
376375
assert set(_2nd_resp["response_args"].keys()) == {
@@ -393,7 +392,7 @@ def test_new_refresh_token(self, conf):
393392
}
394393

395394
areq = AUTH_REQ.copy()
396-
areq["scope"] = ["openid", "offline_access"]
395+
areq["scope"] = ["email"]
397396

398397
session_id = self._create_session(areq)
399398
grant = self.endpoint_context.authz(session_id, areq)
@@ -422,9 +421,124 @@ def test_new_refresh_token(self, conf):
422421

423422
assert first_refresh_token != second_refresh_token
424423

424+
def test_refresh_scopes(self):
425+
areq = AUTH_REQ.copy()
426+
areq["scope"] = ["email", "profile"]
427+
428+
session_id = self._create_session(areq)
429+
grant = self.endpoint_context.authz(session_id, areq)
430+
code = self._mint_code(grant, areq["client_id"])
431+
432+
_token_request = TOKEN_REQ_DICT.copy()
433+
_token_request["code"] = code.value
434+
_req = self.token_endpoint.parse_request(_token_request)
435+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
436+
437+
_request = REFRESH_TOKEN_REQ.copy()
438+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
439+
_request["scope"] = ["email"]
440+
441+
_req = self.token_endpoint.parse_request(_request.to_json())
442+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
443+
assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"}
444+
assert set(_resp["response_args"].keys()) == {
445+
"access_token",
446+
"token_type",
447+
"expires_in",
448+
"refresh_token",
449+
"scope",
450+
}
451+
452+
_token_value = _resp["response_args"]["access_token"]
453+
_session_info = self.session_manager.get_session_info_by_token(_token_value)
454+
at = self.session_manager.find_token(
455+
_session_info["session_id"], _token_value
456+
)
457+
rt = self.session_manager.find_token(
458+
_session_info["session_id"], _resp["response_args"]["refresh_token"]
459+
)
460+
461+
assert at.scope == rt.scope == _request["scope"]
462+
463+
def test_refresh_more_scopes(self):
464+
areq = AUTH_REQ.copy()
465+
areq["scope"] = ["email"]
466+
467+
session_id = self._create_session(areq)
468+
grant = self.endpoint_context.authz(session_id, areq)
469+
code = self._mint_code(grant, areq["client_id"])
470+
471+
_token_request = TOKEN_REQ_DICT.copy()
472+
_token_request["code"] = code.value
473+
_req = self.token_endpoint.parse_request(_token_request)
474+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
475+
476+
_request = REFRESH_TOKEN_REQ.copy()
477+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
478+
_request["scope"] = ["email", "profile"]
479+
480+
_req = self.token_endpoint.parse_request(_request.to_json())
481+
assert isinstance(_req, TokenErrorResponse)
482+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
483+
484+
assert _resp.to_dict() == {
485+
"error": "invalid_request",
486+
"error_description": "Invalid refresh scopes"
487+
}
488+
489+
def test_refresh_more_scopes_2(self):
490+
areq = AUTH_REQ.copy()
491+
areq["scope"] = ["email", "profile"]
492+
493+
session_id = self._create_session(areq)
494+
grant = self.endpoint_context.authz(session_id, areq)
495+
code = self._mint_code(grant, areq["client_id"])
496+
497+
_token_request = TOKEN_REQ_DICT.copy()
498+
_token_request["code"] = code.value
499+
_req = self.token_endpoint.parse_request(_token_request)
500+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
501+
502+
_request = REFRESH_TOKEN_REQ.copy()
503+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
504+
_request["scope"] = ["email"]
505+
506+
_token_value = _resp["response_args"]["refresh_token"]
507+
508+
_req = self.token_endpoint.parse_request(_request.to_json())
509+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
510+
511+
_token_value = _resp["response_args"]["refresh_token"]
512+
_request["refresh_token"] = _token_value
513+
# We should be able to request the original requests scopes
514+
_request["scope"] = ["email", "profile"]
515+
516+
_req = self.token_endpoint.parse_request(_request.to_json())
517+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
518+
519+
assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"}
520+
assert set(_resp["response_args"].keys()) == {
521+
"access_token",
522+
"token_type",
523+
"expires_in",
524+
"refresh_token",
525+
"scope",
526+
}
527+
528+
_token_value = _resp["response_args"]["access_token"]
529+
_session_info = self.session_manager.get_session_info_by_token(_token_value)
530+
at = self.session_manager.find_token(
531+
_session_info["session_id"], _token_value
532+
)
533+
rt = self.session_manager.find_token(
534+
_session_info["session_id"], _resp["response_args"]["refresh_token"]
535+
)
536+
537+
assert at.scope == rt.scope == _request["scope"]
538+
425539
def test_do_refresh_access_token_not_allowed(self):
426540
areq = AUTH_REQ.copy()
427-
areq["scope"] = ["openid", "offline_access"]
541+
areq["scope"] = ["email"]
428542

429543
session_id = self._create_session(areq)
430544
grant = self.endpoint_context.authz(session_id, areq)
@@ -448,7 +562,7 @@ def test_do_refresh_access_token_not_allowed(self):
448562

449563
def test_do_refresh_access_token_revoked(self):
450564
areq = AUTH_REQ.copy()
451-
areq["scope"] = ["openid"]
565+
areq["scope"] = ["email"]
452566

453567
session_id = self._create_session(areq)
454568
grant = self.endpoint_context.authz(session_id, areq)

0 commit comments

Comments
 (0)