diff --git a/src/oidcop/oauth2/token.py b/src/oidcop/oauth2/token.py index 36fb292b..55157040 100755 --- a/src/oidcop/oauth2/token.py +++ b/src/oidcop/oauth2/token.py @@ -53,6 +53,7 @@ def _mint_token( session_id: str, client_id: str, based_on: Optional[SessionToken] = None, + scope: Optional[list] = None, token_args: Optional[dict] = None, token_type: Optional[str] = "" ) -> SessionToken: @@ -80,6 +81,7 @@ def _mint_token( token_handler=_mngr.token_handler[token_class], based_on=based_on, usage_rules=usage_rules, + scope=scope, token_type=token_type, **_args, ) @@ -136,7 +138,6 @@ def process_request(self, req: Union[Message, dict], **kwargs): _log_debug("All checks OK") issue_refresh = kwargs.get("issue_refresh", False) - _response = { "token_type": "Bearer", "scope": grant.scope, @@ -225,15 +226,29 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_value = req["refresh_token"] _session_info = _mngr.get_session_info_by_token(token_value, grant=True) - _grant = _session_info["grant"] + + token_type = "Bearer" + + # Is DPOP supported + if "dpop_signing_alg_values_supported" in _context.provider_info: + _dpop_jkt = req.get("dpop_jkt") + if _dpop_jkt: + _grant.extra["dpop_jkt"] = _dpop_jkt + token_type = "DPoP" + token = _grant.get_token(token_value) + scope = _grant.find_scope(token.based_on) + if "scope" in req: + scope = req["scope"] access_token = self._mint_token( token_class="access_token", grant=_grant, session_id=_session_info["session_id"], client_id=_session_info["client_id"], based_on=token, + scope=scope, + token_type=token_type, ) _resp = { @@ -246,13 +261,15 @@ def process_request(self, req: Union[Message, dict], **kwargs): _resp["expires_in"] = access_token.expires_at - utc_time_sans_frac() _mints = token.usage_rules.get("supports_minting") - if "refresh_token" in _mints: + issue_refresh = kwargs.get("issue_refresh", False) + if "refresh_token" in _mints and issue_refresh: refresh_token = self._mint_token( token_class="refresh_token", grant=_grant, session_id=_session_info["session_id"], client_id=_session_info["client_id"], based_on=token, + scope=scope, ) refresh_token.usage_rules = token.usage_rules.copy() _resp["refresh_token"] = refresh_token.value @@ -288,7 +305,8 @@ def post_parse_request( logger.error("Access Code invalid") return self.error_cls(error="invalid_grant") - token = _session_info["grant"].get_token(request["refresh_token"]) + grant = _session_info["grant"] + token = grant.get_token(request["refresh_token"]) if not isinstance(token, RefreshToken): return self.error_cls(error="invalid_request", error_description="Wrong token type") @@ -298,6 +316,15 @@ def post_parse_request( error="invalid_request", error_description="Refresh token inactive" ) + if "scope" in request: + req_scopes = set(request["scope"]) + scopes = set(grant.find_scope(token.based_on)) + if scopes < req_scopes: + return self.error_cls( + error="invalid_request", + error_description="Invalid refresh scopes", + ) + return request diff --git a/src/oidcop/oidc/token.py b/src/oidcop/oidc/token.py index 0e4dc5e3..0a4aeca9 100755 --- a/src/oidcop/oidc/token.py +++ b/src/oidcop/oidc/token.py @@ -202,13 +202,17 @@ def process_request(self, req: Union[Message, dict], **kwargs): token_type = "DPoP" token = _grant.get_token(token_value) + scope = _grant.find_scope(token.based_on) + if "scope" in req: + scope = req["scope"] access_token = self._mint_token( token_class="access_token", grant=_grant, session_id=_session_info["session_id"], client_id=_session_info["client_id"], based_on=token, - token_type=token_type + scope=scope, + token_type=token_type, ) _resp = { @@ -221,18 +225,25 @@ def process_request(self, req: Union[Message, dict], **kwargs): _resp["expires_in"] = access_token.expires_at - utc_time_sans_frac() _mints = token.usage_rules.get("supports_minting") - if "refresh_token" in _mints: + issue_refresh = False + if "issue_refresh" in kwargs: + issue_refresh = kwargs["issue_refresh"] + else: + if "offline_access" in scope: + issue_refresh = True + if "refresh_token" in _mints and issue_refresh: refresh_token = self._mint_token( token_class="refresh_token", grant=_grant, session_id=_session_info["session_id"], client_id=_session_info["client_id"], based_on=token, + scope=scope, ) refresh_token.usage_rules = token.usage_rules.copy() _resp["refresh_token"] = refresh_token.value - if "id_token" in _mints: + if "id_token" in _mints and "openid" in scope: try: _idtoken = self._mint_token( token_class="refresh_token", @@ -240,6 +251,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): session_id=_session_info["session_id"], client_id=_session_info["client_id"], based_on=token, + scope=scope, ) except (JWEException, NoSuitableSigningKeys) as err: logger.warning(str(err)) @@ -281,7 +293,8 @@ def post_parse_request( logger.error("Access Code invalid") return self.error_cls(error="invalid_grant") - token = _session_info["grant"].get_token(request["refresh_token"]) + grant = _session_info["grant"] + token = grant.get_token(request["refresh_token"]) if not isinstance(token, RefreshToken): return self.error_cls(error="invalid_request", error_description="Wrong token type") @@ -291,6 +304,15 @@ def post_parse_request( error="invalid_request", error_description="Refresh token inactive" ) + if "scope" in request: + req_scopes = set(request["scope"]) + scopes = set(grant.find_scope(token.based_on)) + if scopes < req_scopes: + return self.error_cls( + error="invalid_request", + error_description="Invalid refresh scopes", + ) + return request diff --git a/tests/test_24_oauth2_token_endpoint.py b/tests/test_24_oauth2_token_endpoint.py index b3144bee..6fe115da 100644 --- a/tests/test_24_oauth2_token_endpoint.py +++ b/tests/test_24_oauth2_token_endpoint.py @@ -47,7 +47,7 @@ AUTH_REQ = AuthorizationRequest( client_id="client_1", redirect_uri="https://example.com/cb", - scope=["openid"], + scope=["email"], state="STATE", response_type="code", ) @@ -302,7 +302,7 @@ def test_process_request_using_private_key_jwt(self): def test_do_refresh_access_token(self): areq = AUTH_REQ.copy() - areq["scope"] = ["openid"] + areq["scope"] = ["email"] session_id = self._create_session(areq) grant = self.endpoint_context.authz(session_id, areq) @@ -324,7 +324,7 @@ def test_do_refresh_access_token(self): _token.usage_rules["supports_minting"] = ["access_token", "refresh_token"] _req = self.token_endpoint.parse_request(_request.to_json()) - _resp = self.token_endpoint.process_request(request=_req) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} assert set(_resp["response_args"].keys()) == { "access_token", @@ -338,7 +338,7 @@ def test_do_refresh_access_token(self): def test_do_2nd_refresh_access_token(self): areq = AUTH_REQ.copy() - areq["scope"] = ["openid", "offline_access"] + areq["scope"] = ["email"] session_id = self._create_session(areq) grant = self.endpoint_context.authz(session_id, areq) @@ -361,16 +361,15 @@ def test_do_2nd_refresh_access_token(self): _token.usage_rules["supports_minting"] = [ "access_token", "refresh_token", - "id_token", ] _req = self.token_endpoint.parse_request(_request.to_json()) - _resp = self.token_endpoint.process_request(request=_req) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) _2nd_request = REFRESH_TOKEN_REQ.copy() _2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"] _2nd_req = self.token_endpoint.parse_request(_request.to_json()) - _2nd_resp = self.token_endpoint.process_request(request=_req) + _2nd_resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"} assert set(_2nd_resp["response_args"].keys()) == { @@ -393,7 +392,7 @@ def test_new_refresh_token(self, conf): } areq = AUTH_REQ.copy() - areq["scope"] = ["openid", "offline_access"] + areq["scope"] = ["email"] session_id = self._create_session(areq) grant = self.endpoint_context.authz(session_id, areq) @@ -422,9 +421,124 @@ def test_new_refresh_token(self, conf): assert first_refresh_token != second_refresh_token + def test_refresh_scopes(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["email", "profile"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["email"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope", + } + + _token_value = _resp["response_args"]["access_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + at = self.session_manager.find_token( + _session_info["session_id"], _token_value + ) + rt = self.session_manager.find_token( + _session_info["session_id"], _resp["response_args"]["refresh_token"] + ) + + assert at.scope == rt.scope == _request["scope"] + + def test_refresh_more_scopes(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["email"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["email", "profile"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + assert isinstance(_req, TokenErrorResponse) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + assert _resp.to_dict() == { + "error": "invalid_request", + "error_description": "Invalid refresh scopes" + } + + def test_refresh_more_scopes_2(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["email", "profile"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["email"] + + _token_value = _resp["response_args"]["refresh_token"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _token_value = _resp["response_args"]["refresh_token"] + _request["refresh_token"] = _token_value + # We should be able to request the original requests scopes + _request["scope"] = ["email", "profile"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope", + } + + _token_value = _resp["response_args"]["access_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + at = self.session_manager.find_token( + _session_info["session_id"], _token_value + ) + rt = self.session_manager.find_token( + _session_info["session_id"], _resp["response_args"]["refresh_token"] + ) + + assert at.scope == rt.scope == _request["scope"] + def test_do_refresh_access_token_not_allowed(self): areq = AUTH_REQ.copy() - areq["scope"] = ["openid", "offline_access"] + areq["scope"] = ["email"] session_id = self._create_session(areq) grant = self.endpoint_context.authz(session_id, areq) @@ -448,7 +562,7 @@ def test_do_refresh_access_token_not_allowed(self): def test_do_refresh_access_token_revoked(self): areq = AUTH_REQ.copy() - areq["scope"] = ["openid"] + areq["scope"] = ["email"] session_id = self._create_session(areq) grant = self.endpoint_context.authz(session_id, areq) diff --git a/tests/test_35_oidc_token_endpoint.py b/tests/test_35_oidc_token_endpoint.py index 6824330c..ba4c1dc4 100755 --- a/tests/test_35_oidc_token_endpoint.py +++ b/tests/test_35_oidc_token_endpoint.py @@ -421,6 +421,231 @@ def test_do_2nd_refresh_access_token(self): msg = self.token_endpoint.do_response(request=_req, **_resp) assert isinstance(msg, dict) + def test_refresh_scopes(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access", "profile"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["openid", "offline_access"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "id_token", + "scope", + } + + _token_value = _resp["response_args"]["access_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + at = self.session_manager.find_token( + _session_info["session_id"], _token_value + ) + rt = self.session_manager.find_token( + _session_info["session_id"], _resp["response_args"]["refresh_token"] + ) + + assert at.scope == rt.scope == _request["scope"] + + def test_refresh_more_scopes(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["openid", "offline_access", "profile"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + assert isinstance(_req, TokenErrorResponse) + _resp = self.token_endpoint.process_request(request=_req) + + assert _resp.to_dict() == { + "error": "invalid_request", + "error_description": "Invalid refresh scopes" + } + + def test_refresh_more_scopes_2(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access", "profile"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["openid", "offline_access"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + _request["refresh_token"] = _token_value + # We should be able to request the original requests scopes + _request["scope"] = ["openid", "offline_access", "profile"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "id_token", + "scope", + } + + _token_value = _resp["response_args"]["access_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + at = self.session_manager.find_token( + _session_info["session_id"], _token_value + ) + rt = self.session_manager.find_token( + _session_info["session_id"], _resp["response_args"]["refresh_token"] + ) + + assert at.scope == rt.scope == _request["scope"] + + def test_refresh_no_openid_scope(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["offline_access"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope", + } + + def test_refresh_no_offline_access_scope(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _request["scope"] = ["openid"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "id_token", + "scope", + } + def test_new_refresh_token(self, conf): self.endpoint_context.cdb["client_1"] = { "client_secret": "hemligt",