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

Commit 3ceea51

Browse files
committed
Add parameter to revoke old refresh token upon issuing new
1 parent 1f16205 commit 3ceea51

File tree

5 files changed

+205
-18
lines changed

5 files changed

+205
-18
lines changed

docs/source/contents/conf.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ An example::
574574
},
575575
"refresh": {
576576
"kwargs": {
577-
"lifetime": 86400
577+
"lifetime": 86400,
578+
"revoke_refresh_on_issue": True
578579
}
579580
}
580581
"id_token": {
@@ -754,3 +755,8 @@ allowed_scopes
754755

755756
A list with the scopes that are allowed to be used (defaults to the keys in the
756757
clients scopes_to_claims).
758+
759+
-----------------------
760+
revoke_refresh_on_issue
761+
-----------------------
762+
Configure whether to revoke the refresh token that was used to issue a new refresh token

src/oidcop/oauth2/token.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,19 @@ def process_request(self, req: Union[Message, dict], **kwargs):
287287

288288
token.register_usage()
289289

290+
if ("client_id" in req
291+
and req["client_id"] in _context.cdb
292+
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
293+
):
294+
revoke_refresh = _context.cdb[req["client_id"]].get("revoke_refresh_on_issue")
295+
else:
296+
revoke_refresh =(
297+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"].get("revoke_refresh_on_issue", True)
298+
)
299+
300+
if revoke_refresh:
301+
token.revoke()
302+
290303
return _resp
291304

292305
def post_parse_request(

src/oidcop/oidc/token.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,9 @@ def process_request(self, req: Union[Message, dict], **kwargs):
8181

8282
logger.debug("All checks OK")
8383

84-
issue_refresh = False
85-
if "issue_refresh" in kwargs:
86-
issue_refresh = kwargs["issue_refresh"]
87-
else:
88-
if "offline_access" in grant.scope:
89-
issue_refresh = True
84+
issue_refresh = kwargs.get("issue_refresh", False)
85+
if "offline_access" in grant.scope:
86+
issue_refresh = True
9087

9188
_response = {
9289
"token_type": token_type,
@@ -242,12 +239,12 @@ def process_request(self, req: Union[Message, dict], **kwargs):
242239
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
243240

244241
_mints = token.usage_rules.get("supports_minting")
245-
issue_refresh = False
246-
if "issue_refresh" in kwargs:
247-
issue_refresh = kwargs["issue_refresh"]
248-
else:
249-
if "offline_access" in scope:
250-
issue_refresh = True
242+
243+
issue_refresh = kwargs.get("refresh_token", False)
244+
# The existence of offline_access scope overwrites issue_refresh
245+
if "offline_access" in scope:
246+
issue_refresh = True
247+
251248
if "refresh_token" in _mints and issue_refresh:
252249
refresh_token = self._mint_token(
253250
token_class="refresh_token",
@@ -281,6 +278,19 @@ def process_request(self, req: Union[Message, dict], **kwargs):
281278

282279
token.register_usage()
283280

281+
if ("client_id" in req
282+
and req["client_id"] in _context.cdb
283+
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
284+
):
285+
revoke_refresh = _context.cdb[req["client_id"]].get("revoke_refresh_on_issue")
286+
else:
287+
revoke_refresh =(
288+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"].get("revoke_refresh_on_issue", False)
289+
)
290+
291+
if revoke_refresh:
292+
token.revoke()
293+
284294
return _resp
285295

286296
def post_parse_request(

tests/test_24_oauth2_token_endpoint.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ def test_do_2nd_refresh_access_token(self):
398398
grant = self.endpoint_context.authz(session_id, areq)
399399
code = self._mint_code(grant, areq["client_id"])
400400

401+
_mngr = self.endpoint_context.session_manager
402+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"]["revoke_refresh_on_issue"]=False
401403
_cntx = self.endpoint_context
402404

403405
_token_request = TOKEN_REQ_DICT.copy()
@@ -423,8 +425,7 @@ def test_do_2nd_refresh_access_token(self):
423425
_2nd_request = REFRESH_TOKEN_REQ.copy()
424426
_2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"]
425427
_2nd_req = self.token_endpoint.parse_request(_request.to_json())
426-
_2nd_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
427-
428+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
428429
assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"}
429430
assert set(_2nd_resp["response_args"].keys()) == {
430431
"access_token",
@@ -475,6 +476,83 @@ def test_new_refresh_token(self, conf):
475476

476477
assert first_refresh_token != second_refresh_token
477478

479+
def test_revoke_on_issue_refresh_token(self, conf):
480+
self.endpoint_context.cdb["client_1"] = {
481+
"client_secret": "hemligt",
482+
"redirect_uris": [("https://example.com/cb", None)],
483+
"client_salt": "salted",
484+
"endpoint_auth_method": "client_secret_post",
485+
"response_types": ["code", "token", "code id_token", "id_token"],
486+
}
487+
488+
_mngr = self.endpoint_context.session_manager
489+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"]["revoke_refresh_on_issue"]=True
490+
areq = AUTH_REQ.copy()
491+
areq["scope"] = ["email"]
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+
assert "refresh_token" in _resp["response_args"]
502+
first_refresh_token = _resp["response_args"]["refresh_token"]
503+
504+
_refresh_request = REFRESH_TOKEN_REQ.copy()
505+
_refresh_request["refresh_token"] = first_refresh_token
506+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
507+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
508+
assert "refresh_token" in _2nd_resp["response_args"]
509+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
510+
511+
assert first_refresh_token != second_refresh_token
512+
first_refresh_token = grant.get_token(first_refresh_token)
513+
second_refresh_token = grant.get_token(second_refresh_token)
514+
assert first_refresh_token.revoked is True
515+
assert second_refresh_token.revoked is False
516+
517+
def test_revoke_on_issue_refresh_token_per_client(self, conf):
518+
self.endpoint_context.cdb["client_1"] = {
519+
"client_secret": "hemligt",
520+
"redirect_uris": [("https://example.com/cb", None)],
521+
"client_salt": "salted",
522+
"endpoint_auth_method": "client_secret_post",
523+
"response_types": ["code", "token", "code id_token", "id_token"],
524+
}
525+
self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True
526+
areq = AUTH_REQ.copy()
527+
areq["scope"] = ["openid", "offline_access"]
528+
529+
session_id = self._create_session(areq)
530+
grant = self.endpoint_context.authz(session_id, areq)
531+
code = self._mint_code(grant, areq["client_id"])
532+
533+
_token_request = TOKEN_REQ_DICT.copy()
534+
_token_request["code"] = code.value
535+
_req = self.token_endpoint.parse_request(_token_request)
536+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
537+
assert "refresh_token" in _resp["response_args"]
538+
first_refresh_token = _resp["response_args"]["refresh_token"]
539+
540+
_refresh_request = REFRESH_TOKEN_REQ.copy()
541+
_refresh_request["refresh_token"] = first_refresh_token
542+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
543+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
544+
assert "refresh_token" in _2nd_resp["response_args"]
545+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
546+
547+
_2d_refresh_request = REFRESH_TOKEN_REQ.copy()
548+
_2d_refresh_request["refresh_token"] = second_refresh_token
549+
550+
assert first_refresh_token != second_refresh_token
551+
first_refresh_token = grant.get_token(first_refresh_token)
552+
second_refresh_token = grant.get_token(second_refresh_token)
553+
assert first_refresh_token.revoked is True
554+
assert second_refresh_token.revoked is False
555+
478556
def test_refresh_scopes(self):
479557
areq = AUTH_REQ.copy()
480558
areq["scope"] = ["email", "profile"]
@@ -695,4 +773,4 @@ def test_refresh_token_request_other_client(self):
695773
assert isinstance(_resp, TokenErrorResponse)
696774
assert _resp.to_dict() == {
697775
"error": "invalid_grant", "error_description": "Wrong client"
698-
}
776+
}

tests/test_35_oidc_token_endpoint.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def conf():
117117
},
118118
"refresh": {
119119
"class": "oidcop.token.jwt_token.JWTToken",
120-
"kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], },
120+
"kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"]},
121121
},
122122
"id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}},
123123
},
@@ -390,7 +390,8 @@ def test_do_2nd_refresh_access_token(self):
390390
session_id = self._create_session(areq)
391391
grant = self.endpoint_context.authz(session_id, areq)
392392
code = self._mint_code(grant, areq["client_id"])
393-
393+
_mngr = self.endpoint_context.session_manager
394+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"]["revoke_refresh_on_issue"]=False
394395
_cntx = self.endpoint_context
395396

396397
_token_request = TOKEN_REQ_DICT.copy()
@@ -761,6 +762,85 @@ def test_new_refresh_token(self, conf):
761762

762763
assert first_refresh_token != second_refresh_token
763764

765+
def test_revoke_on_issue_refresh_token(self, conf):
766+
self.endpoint_context.cdb["client_1"] = {
767+
"client_secret": "hemligt",
768+
"redirect_uris": [("https://example.com/cb", None)],
769+
"client_salt": "salted",
770+
"endpoint_auth_method": "client_secret_post",
771+
"response_types": ["code", "token", "code id_token", "id_token"],
772+
}
773+
_mngr = self.endpoint_context.session_manager
774+
_mngr.conf["token_handler_args"]["refresh"]["kwargs"]["revoke_refresh_on_issue"]=True
775+
areq = AUTH_REQ.copy()
776+
areq["scope"] = ["openid", "offline_access"]
777+
778+
session_id = self._create_session(areq)
779+
grant = self.endpoint_context.authz(session_id, areq)
780+
code = self._mint_code(grant, areq["client_id"])
781+
782+
_token_request = TOKEN_REQ_DICT.copy()
783+
_token_request["code"] = code.value
784+
_req = self.token_endpoint.parse_request(_token_request)
785+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
786+
assert "refresh_token" in _resp["response_args"]
787+
first_refresh_token = _resp["response_args"]["refresh_token"]
788+
789+
_refresh_request = REFRESH_TOKEN_REQ.copy()
790+
_refresh_request["refresh_token"] = first_refresh_token
791+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
792+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
793+
assert "refresh_token" in _2nd_resp["response_args"]
794+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
795+
796+
_2d_refresh_request = REFRESH_TOKEN_REQ.copy()
797+
_2d_refresh_request["refresh_token"] = second_refresh_token
798+
799+
assert first_refresh_token != second_refresh_token
800+
first_refresh_token = grant.get_token(first_refresh_token)
801+
second_refresh_token = grant.get_token(second_refresh_token)
802+
assert first_refresh_token.revoked is True
803+
assert second_refresh_token.revoked is False
804+
805+
def test_revoke_on_issue_refresh_token_per_client(self, conf):
806+
self.endpoint_context.cdb["client_1"] = {
807+
"client_secret": "hemligt",
808+
"redirect_uris": [("https://example.com/cb", None)],
809+
"client_salt": "salted",
810+
"endpoint_auth_method": "client_secret_post",
811+
"response_types": ["code", "token", "code id_token", "id_token"],
812+
}
813+
self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True
814+
areq = AUTH_REQ.copy()
815+
areq["scope"] = ["openid", "offline_access"]
816+
817+
session_id = self._create_session(areq)
818+
grant = self.endpoint_context.authz(session_id, areq)
819+
code = self._mint_code(grant, areq["client_id"])
820+
821+
_token_request = TOKEN_REQ_DICT.copy()
822+
_token_request["code"] = code.value
823+
_req = self.token_endpoint.parse_request(_token_request)
824+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
825+
assert "refresh_token" in _resp["response_args"]
826+
first_refresh_token = _resp["response_args"]["refresh_token"]
827+
828+
_refresh_request = REFRESH_TOKEN_REQ.copy()
829+
_refresh_request["refresh_token"] = first_refresh_token
830+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
831+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
832+
assert "refresh_token" in _2nd_resp["response_args"]
833+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
834+
835+
_2d_refresh_request = REFRESH_TOKEN_REQ.copy()
836+
_2d_refresh_request["refresh_token"] = second_refresh_token
837+
838+
assert first_refresh_token != second_refresh_token
839+
first_refresh_token = grant.get_token(first_refresh_token)
840+
second_refresh_token = grant.get_token(second_refresh_token)
841+
assert first_refresh_token.revoked is True
842+
assert second_refresh_token.revoked is False
843+
764844
def test_do_refresh_access_token_not_allowed(self):
765845
areq = AUTH_REQ.copy()
766846
areq["scope"] = ["openid", "offline_access"]

0 commit comments

Comments
 (0)