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

Commit a963c93

Browse files
committed
Allow requesting for scopes in refresh
1 parent dc4be5e commit a963c93

4 files changed

Lines changed: 468 additions & 10 deletions

File tree

src/oidcop/oauth2/token.py

Lines changed: 42 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
)
@@ -135,7 +137,12 @@ def process_request(self, req: Union[Message, dict], **kwargs):
135137

136138
_log_debug("All checks OK")
137139

138-
issue_refresh = kwargs.get("issue_refresh", False)
140+
issue_refresh = False
141+
if "issue_refresh" in kwargs:
142+
issue_refresh = kwargs["issue_refresh"]
143+
else:
144+
if "offline_access" in grant.scope:
145+
issue_refresh = True
139146

140147
_response = {
141148
"token_type": "Bearer",
@@ -225,15 +232,29 @@ def process_request(self, req: Union[Message, dict], **kwargs):
225232

226233
token_value = req["refresh_token"]
227234
_session_info = _mngr.get_session_info_by_token(token_value, grant=True)
228-
229235
_grant = _session_info["grant"]
236+
237+
token_type = "Bearer"
238+
239+
# Is DPOP supported
240+
if "dpop_signing_alg_values_supported" in _context.provider_info:
241+
_dpop_jkt = req.get("dpop_jkt")
242+
if _dpop_jkt:
243+
_grant.extra["dpop_jkt"] = _dpop_jkt
244+
token_type = "DPoP"
245+
230246
token = _grant.get_token(token_value)
247+
scope = _grant.find_scope(token.based_on)
248+
if "scope" in req:
249+
scope = req["scope"]
231250
access_token = self._mint_token(
232251
token_class="access_token",
233252
grant=_grant,
234253
session_id=_session_info["session_id"],
235254
client_id=_session_info["client_id"],
236255
based_on=token,
256+
scope=scope,
257+
token_type=token_type,
237258
)
238259

239260
_resp = {
@@ -246,13 +267,20 @@ def process_request(self, req: Union[Message, dict], **kwargs):
246267
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
247268

248269
_mints = token.usage_rules.get("supports_minting")
249-
if "refresh_token" in _mints:
270+
issue_refresh = False
271+
if "issue_refresh" in kwargs:
272+
issue_refresh = kwargs["issue_refresh"]
273+
else:
274+
if "offline_access" in scope:
275+
issue_refresh = True
276+
if "refresh_token" in _mints and issue_refresh:
250277
refresh_token = self._mint_token(
251278
token_class="refresh_token",
252279
grant=_grant,
253280
session_id=_session_info["session_id"],
254281
client_id=_session_info["client_id"],
255282
based_on=token,
283+
scope=scope,
256284
)
257285
refresh_token.usage_rules = token.usage_rules.copy()
258286
_resp["refresh_token"] = refresh_token.value
@@ -288,7 +316,8 @@ def post_parse_request(
288316
logger.error("Access Code invalid")
289317
return self.error_cls(error="invalid_grant")
290318

291-
token = _session_info["grant"].get_token(request["refresh_token"])
319+
grant = _session_info["grant"]
320+
token = grant.get_token(request["refresh_token"])
292321

293322
if not isinstance(token, RefreshToken):
294323
return self.error_cls(error="invalid_request", error_description="Wrong token type")
@@ -298,6 +327,15 @@ def post_parse_request(
298327
error="invalid_request", error_description="Refresh token inactive"
299328
)
300329

330+
if "scope" in request:
331+
req_scopes = set(request["scope"])
332+
scopes = set(grant.find_scope(token.based_on))
333+
if scopes < req_scopes:
334+
return self.error_cls(
335+
error="invalid_request",
336+
error_description="Invalid refresh scopes",
337+
)
338+
301339
return request
302340

303341

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: 175 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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"] = ["openid", "offline_access"]
306306

307307
session_id = self._create_session(areq)
308308
grant = self.endpoint_context.authz(session_id, areq)
@@ -361,7 +361,6 @@ 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())
@@ -422,6 +421,180 @@ 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"] = ["openid", "offline_access", "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)
436+
437+
_request = REFRESH_TOKEN_REQ.copy()
438+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
439+
_request["scope"] = ["openid", "offline_access"]
440+
441+
_req = self.token_endpoint.parse_request(_request.to_json())
442+
_resp = self.token_endpoint.process_request(request=_req)
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"] = ["openid", "offline_access"]
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)
475+
476+
_request = REFRESH_TOKEN_REQ.copy()
477+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
478+
_request["scope"] = ["openid", "offline_access", "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)
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"] = ["openid", "offline_access", "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)
501+
502+
_request = REFRESH_TOKEN_REQ.copy()
503+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
504+
_request["scope"] = ["openid", "offline_access"]
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)
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"] = ["openid", "offline_access", "profile"]
515+
516+
_req = self.token_endpoint.parse_request(_request.to_json())
517+
_resp = self.token_endpoint.process_request(request=_req)
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+
539+
def test_refresh_no_openid_scope(self):
540+
areq = AUTH_REQ.copy()
541+
areq["scope"] = ["openid", "offline_access"]
542+
543+
session_id = self._create_session(areq)
544+
grant = self.endpoint_context.authz(session_id, areq)
545+
code = self._mint_code(grant, areq["client_id"])
546+
547+
_token_request = TOKEN_REQ_DICT.copy()
548+
_token_request["code"] = code.value
549+
_req = self.token_endpoint.parse_request(_token_request)
550+
_resp = self.token_endpoint.process_request(request=_req)
551+
552+
_request = REFRESH_TOKEN_REQ.copy()
553+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
554+
_request["scope"] = ["offline_access"]
555+
556+
_token_value = _resp["response_args"]["refresh_token"]
557+
558+
_req = self.token_endpoint.parse_request(_request.to_json())
559+
_resp = self.token_endpoint.process_request(request=_req)
560+
561+
assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"}
562+
assert set(_resp["response_args"].keys()) == {
563+
"access_token",
564+
"token_type",
565+
"expires_in",
566+
"refresh_token",
567+
"scope",
568+
}
569+
570+
def test_refresh_no_offline_access_scope(self):
571+
areq = AUTH_REQ.copy()
572+
areq["scope"] = ["openid", "offline_access"]
573+
574+
session_id = self._create_session(areq)
575+
grant = self.endpoint_context.authz(session_id, areq)
576+
code = self._mint_code(grant, areq["client_id"])
577+
578+
_token_request = TOKEN_REQ_DICT.copy()
579+
_token_request["code"] = code.value
580+
_req = self.token_endpoint.parse_request(_token_request)
581+
_resp = self.token_endpoint.process_request(request=_req)
582+
583+
_request = REFRESH_TOKEN_REQ.copy()
584+
_request["refresh_token"] = _resp["response_args"]["refresh_token"]
585+
_request["scope"] = ["openid"]
586+
587+
_req = self.token_endpoint.parse_request(_request.to_json())
588+
_resp = self.token_endpoint.process_request(request=_req)
589+
590+
assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"}
591+
assert set(_resp["response_args"].keys()) == {
592+
"access_token",
593+
"token_type",
594+
"expires_in",
595+
"scope",
596+
}
597+
425598
def test_do_refresh_access_token_not_allowed(self):
426599
areq = AUTH_REQ.copy()
427600
areq["scope"] = ["openid", "offline_access"]

0 commit comments

Comments
 (0)