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

Commit 16e99e6

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

5 files changed

Lines changed: 200 additions & 19 deletions

File tree

docs/source/contents/conf.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,9 @@ An example::
339339
"client_secret_post",
340340
"client_secret_basic",
341341
"client_secret_jwt",
342-
"private_key_jwt"
343-
]
342+
"private_key_jwt",
343+
],
344+
"revoke_refresh_on_issue": True
344345
}
345346
},
346347
"userinfo": {
@@ -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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,17 @@ 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 = self.endpoint.revoke_refresh_on_issue
297+
298+
if revoke_refresh:
299+
token.revoke()
300+
290301
return _resp
291302

292303
def post_parse_request(
@@ -365,6 +376,7 @@ def __init__(self, server_get, new_refresh_token=False, **kwargs):
365376
self.allow_refresh = False
366377
self.new_refresh_token = new_refresh_token
367378
self.configure_grant_types(kwargs.get("grant_types_supported"))
379+
self.revoke_refresh_on_issue = kwargs.get("revoke_refresh_on_issue", False)
368380

369381
def configure_grant_types(self, grant_types_supported):
370382
if grant_types_supported is None:

src/oidcop/oidc/token.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,10 @@ 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", None)
85+
# The existence of offline_access scope overwrites issue_refresh
86+
if issue_refresh is None and "offline_access" in grant.scope:
87+
issue_refresh = True
9088

9189
_response = {
9290
"token_type": token_type,
@@ -242,12 +240,12 @@ def process_request(self, req: Union[Message, dict], **kwargs):
242240
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
243241

244242
_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
243+
244+
issue_refresh = kwargs.get("issue_refresh", None)
245+
# The existence of offline_access scope overwrites issue_refresh
246+
if issue_refresh is None and "offline_access" in scope:
247+
issue_refresh = True
248+
251249
if "refresh_token" in _mints and issue_refresh:
252250
refresh_token = self._mint_token(
253251
token_class="refresh_token",
@@ -281,6 +279,17 @@ def process_request(self, req: Union[Message, dict], **kwargs):
281279

282280
token.register_usage()
283281

282+
if ("client_id" in req
283+
and req["client_id"] in _context.cdb
284+
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
285+
):
286+
revoke_refresh = _context.cdb[req["client_id"]].get("revoke_refresh_on_issue")
287+
else:
288+
revoke_refresh = revoke_refresh = self.endpoint.revoke_refresh_on_issue
289+
290+
if revoke_refresh:
291+
token.revoke()
292+
284293
return _resp
285294

286295
def post_parse_request(

tests/test_24_oauth2_token_endpoint.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ 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+
self.token_endpoint.revoke_refresh_on_issue = False
401402
_cntx = self.endpoint_context
402403

403404
_token_request = TOKEN_REQ_DICT.copy()
@@ -423,8 +424,7 @@ def test_do_2nd_refresh_access_token(self):
423424
_2nd_request = REFRESH_TOKEN_REQ.copy()
424425
_2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"]
425426
_2nd_req = self.token_endpoint.parse_request(_request.to_json())
426-
_2nd_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
427-
427+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
428428
assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"}
429429
assert set(_2nd_resp["response_args"].keys()) == {
430430
"access_token",
@@ -475,6 +475,82 @@ def test_new_refresh_token(self, conf):
475475

476476
assert first_refresh_token != second_refresh_token
477477

478+
def test_revoke_on_issue_refresh_token(self, conf):
479+
self.endpoint_context.cdb["client_1"] = {
480+
"client_secret": "hemligt",
481+
"redirect_uris": [("https://example.com/cb", None)],
482+
"client_salt": "salted",
483+
"endpoint_auth_method": "client_secret_post",
484+
"response_types": ["code", "token", "code id_token", "id_token"],
485+
}
486+
487+
self.token_endpoint.revoke_refresh_on_issue = True
488+
areq = AUTH_REQ.copy()
489+
areq["scope"] = ["email"]
490+
491+
session_id = self._create_session(areq)
492+
grant = self.endpoint_context.authz(session_id, areq)
493+
code = self._mint_code(grant, areq["client_id"])
494+
495+
_token_request = TOKEN_REQ_DICT.copy()
496+
_token_request["code"] = code.value
497+
_req = self.token_endpoint.parse_request(_token_request)
498+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
499+
assert "refresh_token" in _resp["response_args"]
500+
first_refresh_token = _resp["response_args"]["refresh_token"]
501+
502+
_refresh_request = REFRESH_TOKEN_REQ.copy()
503+
_refresh_request["refresh_token"] = first_refresh_token
504+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
505+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
506+
assert "refresh_token" in _2nd_resp["response_args"]
507+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
508+
509+
assert first_refresh_token != second_refresh_token
510+
first_refresh_token = grant.get_token(first_refresh_token)
511+
second_refresh_token = grant.get_token(second_refresh_token)
512+
assert first_refresh_token.revoked is True
513+
assert second_refresh_token.revoked is False
514+
515+
def test_revoke_on_issue_refresh_token_per_client(self, conf):
516+
self.endpoint_context.cdb["client_1"] = {
517+
"client_secret": "hemligt",
518+
"redirect_uris": [("https://example.com/cb", None)],
519+
"client_salt": "salted",
520+
"endpoint_auth_method": "client_secret_post",
521+
"response_types": ["code", "token", "code id_token", "id_token"],
522+
}
523+
self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True
524+
areq = AUTH_REQ.copy()
525+
areq["scope"] = ["openid", "offline_access"]
526+
527+
session_id = self._create_session(areq)
528+
grant = self.endpoint_context.authz(session_id, areq)
529+
code = self._mint_code(grant, areq["client_id"])
530+
531+
_token_request = TOKEN_REQ_DICT.copy()
532+
_token_request["code"] = code.value
533+
_req = self.token_endpoint.parse_request(_token_request)
534+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
535+
assert "refresh_token" in _resp["response_args"]
536+
first_refresh_token = _resp["response_args"]["refresh_token"]
537+
538+
_refresh_request = REFRESH_TOKEN_REQ.copy()
539+
_refresh_request["refresh_token"] = first_refresh_token
540+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
541+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
542+
assert "refresh_token" in _2nd_resp["response_args"]
543+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
544+
545+
_2d_refresh_request = REFRESH_TOKEN_REQ.copy()
546+
_2d_refresh_request["refresh_token"] = second_refresh_token
547+
548+
assert first_refresh_token != second_refresh_token
549+
first_refresh_token = grant.get_token(first_refresh_token)
550+
second_refresh_token = grant.get_token(second_refresh_token)
551+
assert first_refresh_token.revoked is True
552+
assert second_refresh_token.revoked is False
553+
478554
def test_refresh_scopes(self):
479555
areq = AUTH_REQ.copy()
480556
areq["scope"] = ["email", "profile"]
@@ -695,4 +771,4 @@ def test_refresh_token_request_other_client(self):
695771
assert isinstance(_resp, TokenErrorResponse)
696772
assert _resp.to_dict() == {
697773
"error": "invalid_grant", "error_description": "Wrong client"
698-
}
774+
}

tests/test_35_oidc_token_endpoint.py

Lines changed: 80 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,7 @@ 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+
self.token_endpoint.revoke_refresh_on_issue = False
394394
_cntx = self.endpoint_context
395395

396396
_token_request = TOKEN_REQ_DICT.copy()
@@ -761,6 +761,84 @@ def test_new_refresh_token(self, conf):
761761

762762
assert first_refresh_token != second_refresh_token
763763

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

0 commit comments

Comments
 (0)