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

Commit 47120f8

Browse files
authored
Merge pull request #137 from ctriant/revoke_refresh_token_on_issue
Add parameter to revoke old refresh token upon issuing new
2 parents 9eb577b + 16e99e6 commit 47120f8

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
@@ -296,6 +296,17 @@ def process_request(self, req: Union[Message, dict], **kwargs):
296296

297297
token.register_usage()
298298

299+
if ("client_id" in req
300+
and req["client_id"] in _context.cdb
301+
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
302+
):
303+
revoke_refresh = _context.cdb[req["client_id"]].get("revoke_refresh_on_issue")
304+
else:
305+
revoke_refresh = self.endpoint.revoke_refresh_on_issue
306+
307+
if revoke_refresh:
308+
token.revoke()
309+
299310
return _resp
300311

301312
def post_parse_request(
@@ -374,6 +385,7 @@ def __init__(self, server_get, new_refresh_token=False, **kwargs):
374385
self.allow_refresh = False
375386
self.new_refresh_token = new_refresh_token
376387
self.configure_grant_types(kwargs.get("grant_types_supported"))
388+
self.revoke_refresh_on_issue = kwargs.get("revoke_refresh_on_issue", False)
377389

378390
def configure_grant_types(self, grant_types_supported):
379391
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
@@ -86,12 +86,10 @@ def process_request(self, req: Union[Message, dict], **kwargs):
8686

8787
logger.debug("All checks OK")
8888

89-
issue_refresh = False
90-
if "issue_refresh" in kwargs:
91-
issue_refresh = kwargs["issue_refresh"]
92-
else:
93-
if "offline_access" in grant.scope:
94-
issue_refresh = True
89+
issue_refresh = kwargs.get("issue_refresh", None)
90+
# The existence of offline_access scope overwrites issue_refresh
91+
if issue_refresh is None and "offline_access" in grant.scope:
92+
issue_refresh = True
9593

9694
_response = {
9795
"token_type": token_type,
@@ -251,12 +249,12 @@ def process_request(self, req: Union[Message, dict], **kwargs):
251249
_resp["expires_in"] = access_token.expires_at - utc_time_sans_frac()
252250

253251
_mints = token.usage_rules.get("supports_minting")
254-
issue_refresh = False
255-
if "issue_refresh" in kwargs:
256-
issue_refresh = kwargs["issue_refresh"]
257-
else:
258-
if "offline_access" in scope:
259-
issue_refresh = True
252+
253+
issue_refresh = kwargs.get("issue_refresh", None)
254+
# The existence of offline_access scope overwrites issue_refresh
255+
if issue_refresh is None and "offline_access" in scope:
256+
issue_refresh = True
257+
260258
if "refresh_token" in _mints and issue_refresh:
261259
refresh_token = self._mint_token(
262260
token_class="refresh_token",
@@ -290,6 +288,17 @@ def process_request(self, req: Union[Message, dict], **kwargs):
290288

291289
token.register_usage()
292290

291+
if ("client_id" in req
292+
and req["client_id"] in _context.cdb
293+
and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]]
294+
):
295+
revoke_refresh = _context.cdb[req["client_id"]].get("revoke_refresh_on_issue")
296+
else:
297+
revoke_refresh = revoke_refresh = self.endpoint.revoke_refresh_on_issue
298+
299+
if revoke_refresh:
300+
token.revoke()
301+
293302
return _resp
294303

295304
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
@@ -384,6 +384,7 @@ def test_do_2nd_refresh_access_token(self):
384384
grant = self.endpoint_context.authz(session_id, areq)
385385
code = self._mint_code(grant, areq["client_id"])
386386

387+
self.token_endpoint.revoke_refresh_on_issue = False
387388
_cntx = self.endpoint_context
388389

389390
_token_request = TOKEN_REQ_DICT.copy()
@@ -409,8 +410,7 @@ def test_do_2nd_refresh_access_token(self):
409410
_2nd_request = REFRESH_TOKEN_REQ.copy()
410411
_2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"]
411412
_2nd_req = self.token_endpoint.parse_request(_request.to_json())
412-
_2nd_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
413-
413+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
414414
assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"}
415415
assert set(_2nd_resp["response_args"].keys()) == {
416416
"access_token",
@@ -461,6 +461,82 @@ def test_new_refresh_token(self, conf):
461461

462462
assert first_refresh_token != second_refresh_token
463463

464+
def test_revoke_on_issue_refresh_token(self, conf):
465+
self.endpoint_context.cdb["client_1"] = {
466+
"client_secret": "hemligt",
467+
"redirect_uris": [("https://example.com/cb", None)],
468+
"client_salt": "salted",
469+
"endpoint_auth_method": "client_secret_post",
470+
"response_types": ["code", "token", "code id_token", "id_token"],
471+
}
472+
473+
self.token_endpoint.revoke_refresh_on_issue = True
474+
areq = AUTH_REQ.copy()
475+
areq["scope"] = ["email"]
476+
477+
session_id = self._create_session(areq)
478+
grant = self.endpoint_context.authz(session_id, areq)
479+
code = self._mint_code(grant, areq["client_id"])
480+
481+
_token_request = TOKEN_REQ_DICT.copy()
482+
_token_request["code"] = code.value
483+
_req = self.token_endpoint.parse_request(_token_request)
484+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
485+
assert "refresh_token" in _resp["response_args"]
486+
first_refresh_token = _resp["response_args"]["refresh_token"]
487+
488+
_refresh_request = REFRESH_TOKEN_REQ.copy()
489+
_refresh_request["refresh_token"] = first_refresh_token
490+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
491+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
492+
assert "refresh_token" in _2nd_resp["response_args"]
493+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
494+
495+
assert first_refresh_token != second_refresh_token
496+
first_refresh_token = grant.get_token(first_refresh_token)
497+
second_refresh_token = grant.get_token(second_refresh_token)
498+
assert first_refresh_token.revoked is True
499+
assert second_refresh_token.revoked is False
500+
501+
def test_revoke_on_issue_refresh_token_per_client(self, conf):
502+
self.endpoint_context.cdb["client_1"] = {
503+
"client_secret": "hemligt",
504+
"redirect_uris": [("https://example.com/cb", None)],
505+
"client_salt": "salted",
506+
"endpoint_auth_method": "client_secret_post",
507+
"response_types": ["code", "token", "code id_token", "id_token"],
508+
}
509+
self.endpoint_context.cdb[AUTH_REQ["client_id"]]["revoke_refresh_on_issue"] = True
510+
areq = AUTH_REQ.copy()
511+
areq["scope"] = ["openid", "offline_access"]
512+
513+
session_id = self._create_session(areq)
514+
grant = self.endpoint_context.authz(session_id, areq)
515+
code = self._mint_code(grant, areq["client_id"])
516+
517+
_token_request = TOKEN_REQ_DICT.copy()
518+
_token_request["code"] = code.value
519+
_req = self.token_endpoint.parse_request(_token_request)
520+
_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True)
521+
assert "refresh_token" in _resp["response_args"]
522+
first_refresh_token = _resp["response_args"]["refresh_token"]
523+
524+
_refresh_request = REFRESH_TOKEN_REQ.copy()
525+
_refresh_request["refresh_token"] = first_refresh_token
526+
_2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json())
527+
_2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True)
528+
assert "refresh_token" in _2nd_resp["response_args"]
529+
second_refresh_token = _2nd_resp["response_args"]["refresh_token"]
530+
531+
_2d_refresh_request = REFRESH_TOKEN_REQ.copy()
532+
_2d_refresh_request["refresh_token"] = second_refresh_token
533+
534+
assert first_refresh_token != second_refresh_token
535+
first_refresh_token = grant.get_token(first_refresh_token)
536+
second_refresh_token = grant.get_token(second_refresh_token)
537+
assert first_refresh_token.revoked is True
538+
assert second_refresh_token.revoked is False
539+
464540
def test_refresh_scopes(self):
465541
areq = AUTH_REQ.copy()
466542
areq["scope"] = ["email", "profile"]
@@ -681,4 +757,4 @@ def test_refresh_token_request_other_client(self):
681757
assert isinstance(_resp, TokenErrorResponse)
682758
assert _resp.to_dict() == {
683759
"error": "invalid_grant", "error_description": "Wrong client"
684-
}
760+
}

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)