diff --git a/example/flask_op/views.py b/example/flask_op/views.py index a10d41fc..d17b7347 100644 --- a/example/flask_op/views.py +++ b/example/flask_op/views.py @@ -78,14 +78,16 @@ def do_response(endpoint, req_args, error='', **args): if error: if _response_placement == 'body': _log.info('Error Response: {}'.format(info['response'])) - resp = make_response(info['response'], 400) + _http_response_code = info.get('response_code', 400) + resp = make_response(info['response'], _http_response_code) else: # _response_placement == 'url': _log.info('Redirect to: {}'.format(info['response'])) resp = redirect(info['response']) else: if _response_placement == 'body': _log.info('Response: {}'.format(info['response'])) - resp = make_response(info['response'], 200) + _http_response_code = info.get('response_code', 200) + resp = make_response(info['response'], _http_response_code) else: # _response_placement == 'url': _log.info('Redirect to: {}'.format(info['response'])) resp = redirect(info['response']) @@ -166,10 +168,14 @@ def registration(): current_app.server.server_get("endpoint", 'registration')) -@oidc_op_views.route('/registration_api', methods=['GET']) +@oidc_op_views.route('/registration_api', methods=['GET', 'DELETE']) def registration_api(): - return service_endpoint( - current_app.server.server_get("endpoint", 'registration_read')) + if request.method == "DELETE": + return service_endpoint( + current_app.server.server_get("endpoint", 'registration_delete')) + else: + return service_endpoint( + current_app.server.server_get("endpoint", 'registration_read')) @oidc_op_views.route('/authorization') @@ -245,10 +251,14 @@ def service_endpoint(endpoint): err_msg = ResponseMessage(error='invalid_request', error_description=str(err)) return make_response(err_msg.to_json(), 400) - _log.info('request: {}'.format(req_args)) if isinstance(req_args, ResponseMessage) and 'error' in req_args: - return make_response(req_args.to_json(), 400) + _log.info('Error response: {}'.format(req_args)) + _resp = make_response(req_args.to_json(), 400) + if request.method == "POST": + _resp.headers["Content-type"] = "application/json" + return _resp try: + _log.info('request: {}'.format(req_args)) if isinstance(endpoint, Token): args = endpoint.process_request(AccessTokenRequest(**req_args), http_info=http_info) else: diff --git a/src/oidcop/configure.py b/src/oidcop/configure.py index 042593fa..87591022 100755 --- a/src/oidcop/configure.py +++ b/src/oidcop/configure.py @@ -59,7 +59,10 @@ "max_usage": 1, }, "access_token": {}, - "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, + "refresh_token": { + "supports_minting": ["access_token", "refresh_token"], + "expires_in": -1 + }, }, "expires_in": 43200, } @@ -380,7 +383,10 @@ def __init__( "max_usage": 1, }, "access_token": {}, - "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, + "refresh_token": { + "supports_minting": ["access_token", "refresh_token"], + "expires_in": -1 + }, }, "expires_in": 43200, } diff --git a/src/oidcop/constant.py b/src/oidcop/constant.py index c0527b74..99a0f06d 100644 --- a/src/oidcop/constant.py +++ b/src/oidcop/constant.py @@ -1 +1,3 @@ DIVIDER = ";;" + +DEFAULT_TOKEN_LIFETIME = 1800 diff --git a/src/oidcop/cookie_handler.py b/src/oidcop/cookie_handler.py index b36a0c33..7812f825 100755 --- a/src/oidcop/cookie_handler.py +++ b/src/oidcop/cookie_handler.py @@ -167,6 +167,7 @@ def _ver_dec_content(self, parts): try: msg = decrypter.decrypt(ciphertext, iv, tag=tag) except InvalidTag: + LOGGER.debug("Decryption failed") return None p = lv_unpack(msg.decode("utf-8")) @@ -180,6 +181,8 @@ def _ver_dec_content(self, parts): self.sign_key.key, ): return payload, timestamp + else: + LOGGER.debug("Could not verify signature") else: return payload, timestamp return None @@ -247,12 +250,18 @@ def parse_cookie(self, name: str, cookies: List[dict]) -> Optional[List[dict]]: if not cookies: return None + LOGGER.debug("Looking for '{}' cookies".format(name)) res = [] for _cookie in cookies: - if _cookie["name"] == name: - payload, timestamp = self._ver_dec_content(_cookie["value"].split("|")) - value, typ = payload.split("::") - res.append({"value": value, "type": typ, "timestamp": timestamp}) + LOGGER.debug('Cookie: {}'.format(_cookie)) + if "name" in _cookie and _cookie["name"] == name: + _content = self._ver_dec_content(_cookie["value"].split("|")) + if _content: + payload, timestamp = self._ver_dec_content(_cookie["value"].split("|")) + value, typ = payload.split("::") + res.append({"value": value, "type": typ, "timestamp": timestamp}) + else: + LOGGER.debug(f"Could not verify {name} cookie") return res diff --git a/src/oidcop/endpoint.py b/src/oidcop/endpoint.py index 3152cbb3..f5ea2bf8 100755 --- a/src/oidcop/endpoint.py +++ b/src/oidcop/endpoint.py @@ -128,10 +128,6 @@ def __init__(self, server_get: Callable, **kwargs): self.allowed_targets = [self.name] self.client_verification_method = [] - def parse_cookies(self, cookies: List[dict], context: EndpointContext, name: str): - res = context.cookie_handler.parse_cookie(name, cookies) - return res - def parse_request( self, request: Union[Message, dict, str], http_info: Optional[dict] = None, **kwargs ): @@ -330,10 +326,9 @@ def do_response( resp = None if error: _response = ResponseMessage(error=error) - try: - _response["error_description"] = kwargs["error_description"] - except KeyError: - pass + for attr in ["error_description", "error_uri", "state"]: + if attr in kwargs: + _response[attr] = kwargs[attr] elif "response_msg" in kwargs: resp = kwargs["response_msg"] _response_placement = kwargs.get("response_placement") @@ -405,6 +400,11 @@ def do_response( except KeyError: pass + try: + _resp["response_code"] = kwargs["response_code"] + except KeyError: + pass + return _resp def allowed_target_uris(self): diff --git a/src/oidcop/oauth2/authorization.py b/src/oidcop/oauth2/authorization.py index 2a68230e..41339e98 100755 --- a/src/oidcop/oauth2/authorization.py +++ b/src/oidcop/oauth2/authorization.py @@ -45,9 +45,11 @@ # For the time being. This is JAR specific and should probably be configurable. ALG_PARAMS = { - "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported",], - "enc_alg": ["request_object_encryption_alg", "request_object_encryption_alg_values_supported",], - "enc_enc": ["request_object_encryption_enc", "request_object_encryption_enc_values_supported",], + "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported", ], + "enc_alg": ["request_object_encryption_alg", + "request_object_encryption_alg_values_supported", ], + "enc_enc": ["request_object_encryption_enc", + "request_object_encryption_enc_values_supported", ], } FORM_POST = """ @@ -79,10 +81,10 @@ def max_age(request): def verify_uri( - endpoint_context: EndpointContext, - request: Union[dict, Message], - uri_type: str, - client_id: Optional[str] = None, + endpoint_context: EndpointContext, + request: Union[dict, Message], + uri_type: str, + client_id: Optional[str] = None, ): """ A redirect URI @@ -100,8 +102,6 @@ def verify_uri( if not _cid: logger.error("No client id found") raise UnknownClient("No client_id provided") - else: - logger.debug("Client ID: {}".format(_cid)) _uri = request.get(uri_type) if _uri is None: @@ -122,7 +122,6 @@ def verify_uri( if client_info is None: raise KeyError("No such client") - logger.debug("Client info: {}".format(client_info)) redirect_uris = client_info.get("{}s".format(uri_type)) if redirect_uris is None: raise ValueError(f"No registered {uri_type} for {_cid}") @@ -207,7 +206,7 @@ def get_uri(endpoint_context, request, uri_type): def authn_args_gather( - request: Union[AuthorizationRequest, dict], authn_class_ref: str, cinfo: dict, **kwargs, + request: Union[AuthorizationRequest, dict], authn_class_ref: str, cinfo: dict, **kwargs, ): """ Gather information to be used by the authentication method @@ -291,6 +290,13 @@ def filter_request(self, endpoint_context, req): def extra_response_args(self, aresp): return aresp + def authentication_error_response(self, request, error, error_description, **kwargs): + _error_msg = self.error_cls(error=error, error_description=error_description) + _state = request.get("state") + if _state: + _error_msg["state"] = _state + return _error_msg + def verify_response_type(self, request: Union[Message, dict], cinfo: dict) -> bool: # Checking response types _registered = [set(rt.split(" ")) for rt in cinfo.get("response_types", [])] @@ -392,20 +398,23 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs): """ if not request: logger.debug("No AuthzRequest") - return self.error_cls( - error="invalid_request", error_description="Can not parse AuthzRequest" - ) + return self.authentication_error_response(request, + error="invalid_request", + error_description="Can not parse AuthzRequest" + ) request = self.filter_request(endpoint_context, request) _cinfo = endpoint_context.cdb.get(client_id) if not _cinfo: logger.error("Client ID ({}) not in client database".format(request["client_id"])) - return self.error_cls(error="unauthorized_client", error_description="unknown client") + return self.authentication_error_response(request, error="unauthorized_client", + error_description="unknown client") # Is the asked for response_type among those that are permitted if not self.verify_response_type(request, _cinfo): - return self.error_cls( + return self.authentication_error_response( + request, error="invalid_request", error_description="Trying to use unregistered response_type", ) @@ -414,7 +423,8 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs): try: redirect_uri = get_uri(endpoint_context, request, "redirect_uri") except (RedirectURIError, ParameterError) as err: - return self.error_cls( + return self.authentication_error_response( + request, error="invalid_request", error_description="{}:{}".format(err.__class__.__name__, err), ) @@ -452,7 +462,7 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs): def create_session(self, request, user_id, acr, time_stamp, authn_method): _context = self.server_get("endpoint_context") _mngr = _context.session_manager - authn_event = create_authn_event(user_id, authn_info=acr, time_stamp=time_stamp,) + authn_event = create_authn_event(user_id, authn_info=acr, time_stamp=time_stamp, ) _exp_in = authn_method.kwargs.get("expires_in") if _exp_in and "valid_until" in authn_event: authn_event["valid_until"] = utc_time_sans_frac() + _exp_in @@ -466,14 +476,26 @@ def create_session(self, request, user_id, acr, time_stamp, authn_method): token_usage_rules=_token_usage_rules, ) + def _login_required_error(self, redirect_uri, request): + _res = { + "error": "login_required", + "return_uri": redirect_uri, + "return_type": request["response_type"], + } + _state = request.get("state") + if _state: + _res["state"] = _state + logger.debug("Login required error: {}".format(_res)) + return _res + def setup_auth( - self, - request: Optional[Union[Message, dict]], - redirect_uri: str, - cinfo: dict, - cookie: List[dict] = None, - acr: str = None, - **kwargs, + self, + request: Optional[Union[Message, dict]], + redirect_uri: str, + cinfo: dict, + cookie: List[dict] = None, + acr: str = None, + **kwargs, ): """ @@ -499,6 +521,7 @@ def setup_auth( _max_age = 0 else: _max_age = max_age(request) + logger.debug(f'Max age: {_max_age}') identity, _ts = authn.authenticated_as( client_id, cookie, authorization=_auth_info, max_age=_max_age ) @@ -541,11 +564,7 @@ def setup_auth( if "prompt" in request and "none" in request["prompt"]: # Need to authenticate but not allowed - return { - "error": "login_required", - "return_uri": redirect_uri, - "return_type": request["response_type"], - } + return self._login_required_error(redirect_uri, request) else: return {"function": authn, "args": authn_args} else: @@ -560,11 +579,7 @@ def setup_auth( if user != kwargs["req_user"]: logger.debug("Wanted to be someone else!") if "prompt" in request and "none" in request["prompt"]: - # Need to authenticate but not allowed - return { - "error": "login_required", - "return_uri": redirect_uri, - } + return self._login_required_error(redirect_uri, request) else: return {"function": authn, "args": authn_args} @@ -605,12 +620,12 @@ def aresp_check(self, aresp, request): return "" def response_mode( - self, - request: Union[dict, AuthorizationRequest], - response_args: Optional[AuthorizationResponse] = None, - return_uri: Optional[str] = "", - fragment_enc: Optional[bool] = None, - **kwargs, + self, + request: Union[dict, AuthorizationRequest], + response_args: Optional[AuthorizationResponse] = None, + return_uri: Optional[str] = "", + fragment_enc: Optional[bool] = None, + **kwargs, ) -> dict: resp_mode = request["response_mode"] if resp_mode == "form_post": @@ -618,9 +633,9 @@ def response_mode( _args = response_args.to_dict() else: _args = response_args - msg = FORM_POST.format(inputs=inputs(_args), action=return_uri,) + msg = FORM_POST.format(inputs=inputs(_args), action=return_uri, ) kwargs.update( - {"response_msg": msg, "content_type": "text/html", "response_placement": "body",} + {"response_msg": msg, "content_type": "text/html", "response_placement": "body", } ) elif resp_mode == "fragment": if fragment_enc is False: @@ -640,8 +655,10 @@ def response_mode( return kwargs - def error_response(self, response_info, error, error_description): - resp = self.error_cls(error=error, error_description=str(error_description)) + def error_response(self, response_info, request, error, error_description): + resp = self.authentication_error_response(request, + error=error, + error_description=str(error_description)) response_info["response_args"] = resp return response_info @@ -715,7 +732,8 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict # id_token = _context.idtoken.make(sid, **kwargs) except (JWEException, NoSuitableSigningKeys) as err: logger.warning(str(err)) - resp = self.error_cls( + resp = self.authentication_error_response( + request, error="invalid_request", error_description="Could not sign/encrypt id_token", ) @@ -726,7 +744,8 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict not_handled = rtype.difference(handled_response_type) if not_handled: - resp = self.error_cls( + resp = self.authentication_error_response( + request, error="invalid_request", error_description="unsupported_response_type", ) return {"response_args": resp, "fragment_enc": fragment_enc} @@ -753,13 +772,14 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** grant = _context.authz(session_id, request=request) if grant.is_active() is False: - return self.error_response(response_info, "server_error", "Grant not usable") + return self.error_response(response_info, request, "server_error", "Grant not usable") user_id, client_id, grant_id = _mngr.decrypt_session_id(session_id) try: _mngr.set([user_id, client_id, grant_id], grant) except Exception as err: - return self.error_response(response_info, "server_error", "{}".format(err.args)) + return self.error_response(response_info, request, "server_error", + "{}".format(err.args)) logger.debug("response type: %s" % request["response_type"]) @@ -771,7 +791,8 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** try: redirect_uri = get_uri(_context, request, "redirect_uri") except (RedirectURIError, ParameterError) as err: - return self.error_response(response_info, "invalid_request", "{}".format(err.args)) + return self.error_response(response_info, request, "invalid_request", + "{}".format(err.args)) else: response_info["return_uri"] = redirect_uri @@ -783,7 +804,8 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** try: response_info = self.response_mode(request, **response_info) except InvalidRequest as err: - return self.error_response(response_info, "invalid_request", "{}".format(err.args)) + return self.error_response(response_info, request, "invalid_request", + "{}".format(err.args)) _cookie_info = _context.new_cookie( name=_context.cookie_handler.name["session"], @@ -807,7 +829,7 @@ def authz_part2(self, request, session_id, **kwargs): try: resp_info = self.post_authentication(request, session_id, **kwargs) except Exception as err: - return self.error_response({}, "server_error", err) + return self.error_response({}, request, "server_error", err) _context = self.server_get("endpoint_context") @@ -816,10 +838,11 @@ def authz_part2(self, request, session_id, **kwargs): try: authn_event = _context.session_manager.get_authentication_event(session_id) except KeyError: - return self.error_response({}, "server_error", "No such session") + return self.error_response({}, request, "server_error", "No such session") else: if authn_event.is_valid() is False: - return self.error_response({}, "server_error", "Authentication has timed out") + return self.error_response({}, request, "server_error", + "Authentication has timed out") _state = b64e(as_bytes(json.dumps({"authn_time": authn_event["authn_time"]}))) @@ -859,10 +882,10 @@ def do_request_user(self, request_info, **kwargs): return kwargs def process_request( - self, - request: Optional[Union[Message, dict]] = None, - http_info: Optional[dict] = None, - **kwargs, + self, + request: Optional[Union[Message, dict]] = None, + http_info: Optional[dict] = None, + **kwargs, ): """ The AuthorizationRequest endpoint @@ -877,7 +900,7 @@ def process_request( _cid = request["client_id"] _context = self.server_get("endpoint_context") cinfo = _context.cdb[_cid] - logger.debug("client {}: {}".format(_cid, cinfo)) + # logger.debug("client {}: {}".format(_cid, cinfo)) # this apply the default optionally deny_unknown_scopes policy if cinfo: @@ -888,11 +911,15 @@ def process_request( _cookies = http_info.get("cookie") if _cookies: - _cookies = _context.cookie_handler.parse_cookie("oidcop", _cookies) + logger.debug("parse_cookie@process_request") + _session_cookie_name = _context.cookie_handler.name["session"] + _my_cookies = _context.cookie_handler.parse_cookie(_session_cookie_name, _cookies) + else: + _my_cookies = {} kwargs = self.do_request_user(request_info=request, **kwargs) - info = self.setup_auth(request, request["redirect_uri"], cinfo, _cookies, **kwargs) + info = self.setup_auth(request, request["redirect_uri"], cinfo, _my_cookies, **kwargs) if "error" in info: return info @@ -901,7 +928,7 @@ def process_request( if not _function: logger.debug("- authenticated -") logger.debug("AREQ keys: %s" % request.keys()) - return self.authz_part2(request=request, cookie=_cookies, **info) + return self.authz_part2(request=request, cookie=_my_cookies, **info) try: # Run the authentication function @@ -934,11 +961,19 @@ def __call__(self, client_id, endpoint_context, alg, alg_type): def re_authenticate(request, authn) -> bool: """ - This is where you can demand reauthentication even though the authentication in use + This is where you can demand re-authentication even though the authentication in use is still valid. :param request: :param authn: :return: """ + logger.debug("Re-authenticate ??: {}".format(request)) + + _prompt = request.get("prompt", []) + logger.debug(f"Prompt={_prompt}") + if "login" in _prompt: + logger.debug("Reauthenticate due to prompt=login") + return True + return False diff --git a/src/oidcop/oauth2/token.py b/src/oidcop/oauth2/token.py index 2d4d8cf9..c06199fc 100755 --- a/src/oidcop/oauth2/token.py +++ b/src/oidcop/oauth2/token.py @@ -12,6 +12,7 @@ from oidcmsg.time_util import time_sans_frac from oidcop import sanitize +from oidcop.constant import DEFAULT_TOKEN_LIFETIME from oidcop.endpoint import Endpoint from oidcop.exception import ProcessError from oidcop.session.grant import AuthorizationCode @@ -61,7 +62,7 @@ def _mint_token( if usage_rules: _exp_in = usage_rules.get("expires_in") else: - _exp_in = 0 + _exp_in = DEFAULT_TOKEN_LIFETIME token_args = token_args or {} for meth in _context.token_args_methods: @@ -105,9 +106,8 @@ def process_request(self, req: Union[Message, dict], **kwargs): :return: """ _context = self.endpoint.server_get("endpoint_context") - _mngr = _context.session_manager - _log_debug = logger.debug + logger.debug("Access Token") if req["grant_type"] != "authorization_code": return self.error_cls(error="invalid_request", error_description="Unknown grant_type") @@ -118,6 +118,11 @@ def process_request(self, req: Union[Message, dict], **kwargs): return self.error_cls(error="invalid_request", error_description="Missing code") _session_info = _mngr.get_session_info_by_token(_access_code, grant=True) + if _session_info["client_id"] != req["client_id"]: + logger.debug("{} owner of token".format(_session_info["client_id"])) + logger.warning("Client using token it was not given") + return self.error_cls(error="invalid_grant", error_description="Wrong client") + grant = _session_info["grant"] _based_on = grant.get_token(_access_code) @@ -133,7 +138,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): error="invalid_request", error_description="redirect_uri mismatch" ) - _log_debug("All checks OK") + logger.debug("All checks OK") issue_refresh = kwargs.get("issue_refresh", False) _response = { @@ -218,12 +223,20 @@ class RefreshTokenHelper(TokenEndpointHelper): def process_request(self, req: Union[Message, dict], **kwargs): _context = self.endpoint.server_get("endpoint_context") _mngr = _context.session_manager + logger.debug("Refresh Token") if req["grant_type"] != "refresh_token": return self.error_cls(error="invalid_request", error_description="Wrong grant_type") token_value = req["refresh_token"] _session_info = _mngr.get_session_info_by_token(token_value, grant=True) + logger.debug("Session info: {}".format(_session_info)) + + if _session_info["client_id"] != req["client_id"]: + logger.debug("{} owner of token".format(_session_info["client_id"])) + logger.warning("Client using token it was not given") + return self.error_cls(error="invalid_grant", error_description="Wrong client") + _grant = _session_info["grant"] token_type = "Bearer" diff --git a/src/oidcop/oidc/registration.py b/src/oidcop/oidc/registration.py index 71aa7374..cc67dcd1 100755 --- a/src/oidcop/oidc/registration.py +++ b/src/oidcop/oidc/registration.py @@ -473,4 +473,4 @@ def process_request(self, request=None, new_id=True, set_secret=True, **kwargs): name=_context.cookie_handler.name["register"], client_id=reg_resp["client_id"], ) - return {"response_args": reg_resp, "cookie": _cookie} + return {"response_args": reg_resp, "cookie": _cookie, "response_code": 201} diff --git a/src/oidcop/oidc/session.py b/src/oidcop/oidc/session.py index b3db0e42..4e35141e 100644 --- a/src/oidcop/oidc/session.py +++ b/src/oidcop/oidc/session.py @@ -240,6 +240,7 @@ def process_request( _session_info = None if _cookies: + logger.debug("parse_cookie@session") _cookie_name = _context.cookie_handler.name["session"] try: _cookie_infos = _context.cookie_handler.parse_cookie( diff --git a/src/oidcop/oidc/token.py b/src/oidcop/oidc/token.py index a88f45c8..61c9e878 100755 --- a/src/oidcop/oidc/token.py +++ b/src/oidcop/oidc/token.py @@ -32,7 +32,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): _context = self.endpoint.server_get("endpoint_context") _mngr = _context.session_manager - _log_debug = logger.debug + logger.debug("OIDC Access Token") if req["grant_type"] != "authorization_code": return self.error_cls(error="invalid_request", error_description="Unknown grant_type") @@ -43,6 +43,13 @@ def process_request(self, req: Union[Message, dict], **kwargs): return self.error_cls(error="invalid_request", error_description="Missing code") _session_info = _mngr.get_session_info_by_token(_access_code, grant=True) + logger.debug(f"Session info: {_session_info}") + + if _session_info["client_id"] != req["client_id"]: + logger.debug("{} owner of token".format(_session_info["client_id"])) + logger.warning("{} using token it was not given".format(req["client_id"])) + return self.error_cls(error="invalid_grant", error_description="Wrong client") + grant = _session_info["grant"] token_type = "Bearer" @@ -72,7 +79,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): error="invalid_request", error_description="redirect_uri mismatch" ) - _log_debug("All checks OK") + logger.debug("All checks OK") issue_refresh = False if "issue_refresh" in kwargs: @@ -145,7 +152,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): return _response def post_parse_request( - self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ): """ This is where clients come to get their access tokens @@ -167,8 +174,13 @@ def post_parse_request( if not isinstance(code, AuthorizationCode): return self.error_cls(error="invalid_request", error_description="Wrong token type") + if code.used: # Has been used already + # invalidate all tokens that has been minted using this code + grant.revoke_token(based_on=request["code"], recursive=True) + return self.error_cls(error="invalid_grant", error_description="Code inactive") + if code.is_active() is False: - return self.error_cls(error="invalid_request", error_description="Code inactive") + return self.error_cls(error="invalid_grant", error_description="Code inactive") _auth_req = grant.authorization_request @@ -190,6 +202,11 @@ 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) + if _session_info["client_id"] != req["client_id"]: + logger.debug("{} owner of token".format(_session_info["client_id"])) + logger.warning("{} using token it was not given".format(req["client_id"])) + return self.error_cls(error="invalid_grant", error_description="Wrong client") + _grant = _session_info["grant"] token_type = "Bearer" @@ -267,7 +284,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): return _resp def post_parse_request( - self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs ): """ This is where clients come to refresh their access tokens diff --git a/src/oidcop/oidc/userinfo.py b/src/oidcop/oidc/userinfo.py index 1b514c01..aabc94d8 100755 --- a/src/oidcop/oidc/userinfo.py +++ b/src/oidcop/oidc/userinfo.py @@ -32,7 +32,7 @@ class UserInfo(Endpoint): "userinfo_signing_alg_values_supported": None, "userinfo_encryption_alg_values_supported": None, "userinfo_encryption_enc_values_supported": None, - "client_authn_method": ["bearer_header"], + "client_authn_method": ["bearer_header", "bearer_body"], } def __init__(self, server_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs): diff --git a/src/oidcop/token/__init__.py b/src/oidcop/token/__init__.py index a9bcd791..9134bf55 100755 --- a/src/oidcop/token/__init__.py +++ b/src/oidcop/token/__init__.py @@ -54,11 +54,10 @@ def __call__(self, session_id: Optional[str] = "", ttype: Optional[str] = "", ** def info(self, token): """ - Return type of Token (A=Access code, T=Token, R=Refresh token) and - the session id. + Return dictionary with token information. :param token: A token - :return: tuple of token type and session id + :return: Dictionary with information about the token """ raise NotImplementedError() diff --git a/src/oidcop/token/jwt_token.py b/src/oidcop/token/jwt_token.py index d19024c9..7d4f12b5 100644 --- a/src/oidcop/token/jwt_token.py +++ b/src/oidcop/token/jwt_token.py @@ -7,12 +7,10 @@ from oidcop.exception import ToOld from oidcop.token import Crypt from oidcop.token.exception import WrongTokenClass - from . import Token from . import is_expired from .exception import UnknownToken - -# TYPE_MAP = {"A": "code", "T": "access_token", "R": "refresh_token"} +from ..constant import DEFAULT_TOKEN_LIFETIME class JWTToken(Token): @@ -23,7 +21,7 @@ def __init__( issuer: str = None, aud: Optional[list] = None, alg: str = "ES256", - lifetime: int = 300, + lifetime: int = DEFAULT_TOKEN_LIFETIME, server_get: Callable = None, token_type: str = "Bearer", password: str = "", diff --git a/src/oidcop/user_authn/user.py b/src/oidcop/user_authn/user.py index b8baba08..5b5bdded 100755 --- a/src/oidcop/user_authn/user.py +++ b/src/oidcop/user_authn/user.py @@ -5,12 +5,13 @@ import logging import sys import time -import warnings from typing import List from urllib.parse import unquote +import warnings from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptojwt.jwt import JWT +from oidcmsg.time_util import utc_time_sans_frac from oidcop.exception import FailedAuthentication from oidcop.exception import ImproperlyConfigured @@ -65,6 +66,15 @@ def authenticated_as(self, client_id, cookie=None, **kwargs): return None, 0 else: _info = self.cookie_info(cookie, client_id) + logger.debug('Cookie info: {}'.format(_info)) + if _info: + if 'max_age' in kwargs and kwargs["max_age"] != 0: + _max_age = kwargs["max_age"] + _now = utc_time_sans_frac() + if _now > _info["timestamp"] + _max_age: + logger.debug( + "Too old by {} seconds".format(_now - (_info["timestamp"] + _max_age))) + return None, 0 return _info, time.time() def verify(self, *args, **kwargs): @@ -93,22 +103,30 @@ def done(self, areq): def cookie_info(self, cookie: List[dict], client_id: str) -> dict: _context = self.server_get("endpoint_context") try: + logger.debug("parse_cookie@UserAuthnMethod") vals = _context.cookie_handler.parse_cookie( cookies=cookie, name=_context.cookie_handler.name["session"] ) except (InvalidCookieSign, AssertionError, AttributeError) as err: logger.warning(err) - vals = None + vals = [] + + logger.debug("Value cookies: {}".format(vals)) if vals is None: pass else: for val in vals: _info = json.loads(val["value"]) - _, cid, _ = _context.session_manager.decrypt_session_id(_info["sid"]) - if cid != client_id: + _info["timestamp"] = int(val["timestamp"]) + session_id = _context.session_manager.decrypt_session_id(_info["sid"]) + logger.debug("session id: {}".format(session_id)) + # _, cid, _ = _context.session_manager.decrypt_session_id(_info["sid"]) + if session_id[1] != client_id: continue else: + _info["uid"] = session_id[0] + _info["grant_id"] = session_id[2] return _info return {} @@ -130,13 +148,13 @@ class UserPassJinja2(UserAuthnMethod): url_endpoint = "/verify/user_pass_jinja" def __init__( - self, - db, - template_handler, - template="user_pass.jinja2", - server_get=None, - verify_endpoint="", - **kwargs, + self, + db, + template_handler, + template="user_pass.jinja2", + server_get=None, + verify_endpoint="", + **kwargs, ): super(UserPassJinja2, self).__init__(server_get=server_get) diff --git a/tests/test_12_user_authn.py b/tests/test_12_user_authn.py index 14585267..3cf89fcc 100644 --- a/tests/test_12_user_authn.py +++ b/tests/test_12_user_authn.py @@ -100,7 +100,8 @@ def test_authenticated_as_with_cookie(self): ) _info, _time_stamp = method.authenticated_as("client 12345", [_cookie]) - assert set(_info.keys()) == {"sub", "sid", "state", "client_id"} + assert set(_info.keys()) == {'sub', 'uid', 'state', 'grant_id', 'timestamp', 'sid', + 'client_id'} assert _info["sub"] == "diana" def test_userpassjinja2(self): diff --git a/tests/test_23_oidc_registration_endpoint.py b/tests/test_23_oidc_registration_endpoint.py index eb2c23f2..f55e2bc8 100755 --- a/tests/test_23_oidc_registration_endpoint.py +++ b/tests/test_23_oidc_registration_endpoint.py @@ -178,6 +178,8 @@ def test_process_request(self): _reg_resp = _resp["response_args"] assert isinstance(_reg_resp, RegistrationResponse) assert "client_id" in _reg_resp and "client_secret" in _reg_resp + assert "response_code" in _resp + assert _resp["response_code"] == 201 def test_do_response(self): _req = self.endpoint.parse_request(CLI_REQ.to_json()) @@ -194,6 +196,7 @@ def test_do_response(self): assert isinstance(msg, dict) _msg = json.loads(msg["response"]) assert _msg + assert "response_code" in msg def test_register_unsupported_algo(self): _msg = MSG.copy() diff --git a/tests/test_24_oauth2_token_endpoint.py b/tests/test_24_oauth2_token_endpoint.py index 1d7a6104..0f2bf2fc 100644 --- a/tests/test_24_oauth2_token_endpoint.py +++ b/tests/test_24_oauth2_token_endpoint.py @@ -648,3 +648,53 @@ def test_configure_grant_types(self): assert len(self.token_endpoint.helper) == 1 assert "access_token" in self.token_endpoint.helper assert "refresh_token" not in self.token_endpoint.helper + + def test_token_request_other_client(self): + _context = self.endpoint_context + _context.cdb["client_2"] = _context.cdb["client_1"] + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["client_id"] = "client_2" + _token_request["code"] = code.value + + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + assert isinstance(_resp, TokenErrorResponse) + assert _resp.to_dict() == { + "error": "invalid_grant", "error_description": "Wrong client" + } + + def test_refresh_token_request_other_client(self): + _context = self.endpoint_context + _context.cdb["client_2"] = _context.cdb["client_1"] + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["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["client_id"] = "client_2" + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + + _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"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req, ) + assert isinstance(_resp, TokenErrorResponse) + assert _resp.to_dict() == { + "error": "invalid_grant", "error_description": "Wrong client" + } \ No newline at end of file diff --git a/tests/test_24_oidc_authorization_endpoint.py b/tests/test_24_oidc_authorization_endpoint.py index fa671abd..c877a069 100755 --- a/tests/test_24_oidc_authorization_endpoint.py +++ b/tests/test_24_oidc_authorization_endpoint.py @@ -894,7 +894,7 @@ def test_do_request_uri(self): def test_post_parse_request(self): endpoint_context = self.endpoint.server_get("endpoint_context") - msg = self.endpoint._post_parse_request(None, "client_1", endpoint_context) + msg = self.endpoint._post_parse_request({}, "client_1", endpoint_context) assert "error" in msg request = AuthorizationRequest( diff --git a/tests/test_26_oidc_userinfo_endpoint.py b/tests/test_26_oidc_userinfo_endpoint.py index cc7c082a..03db905a 100755 --- a/tests/test_26_oidc_userinfo_endpoint.py +++ b/tests/test_26_oidc_userinfo_endpoint.py @@ -125,7 +125,7 @@ def create_endpoint(self): "class": userinfo.UserInfo, "kwargs": { "claim_types_supported": ["normal", "aggregated", "distributed", ], - "client_authn_method": ["bearer_header"], + "client_authn_method": ["bearer_header", "bearer_body"], }, }, }, @@ -441,6 +441,23 @@ def test_userinfo_claims_acr_none(self): _response = json.loads(res["response"]) assert _response["acr"] == _acr + def test_userinfo_claims_post(self): + _acr = "https://refeds.org/profile/mfa" + _auth_req = AUTH_REQ.copy() + _auth_req["claims"] = {"userinfo": {"acr": {"value": _acr}}} + + session_id = self._create_session(_auth_req, authn_info=_acr) + grant = self.session_manager[session_id] + code = self._mint_code(grant, session_id) + access_token = self._mint_token("access_token", grant, session_id, code) + + _req = self.endpoint.parse_request({"access_token": access_token.value}) + args = self.endpoint.process_request(_req) + assert args + res = self.endpoint.do_response(request=_req, **args) + _response = json.loads(res["response"]) + assert _response["acr"] == _acr + def test_process_request_absent_userinfo_conf(self): # consider to have a configuration without userinfo defined in ec = self.endpoint.server_get('endpoint_context') diff --git a/tests/test_35_oidc_token_endpoint.py b/tests/test_35_oidc_token_endpoint.py index f8336310..e4c203ea 100755 --- a/tests/test_35_oidc_token_endpoint.py +++ b/tests/test_35_oidc_token_endpoint.py @@ -284,6 +284,7 @@ def test_process_request(self): assert _resp assert set(_resp.keys()) == {"cookie", "http_headers", "response_args"} + assert "expires_in" in _resp["response_args"] def test_process_request_using_code_twice(self): session_id = self._create_session(AUTH_REQ) @@ -837,6 +838,55 @@ def test_access_token_lifetime(self): assert access_token["exp"] - access_token["iat"] == lifetime + def test_token_request_other_client(self): + _context = self.endpoint_context + _context.cdb["client_2"] = _context.cdb["client_1"] + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["client_id"] = "client_2" + _token_request["code"] = code.value + + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + assert isinstance(_resp, TokenErrorResponse) + assert _resp.to_dict() == { + "error": "invalid_grant", "error_description": "Wrong client" + } + + def test_refresh_token_request_other_client(self): + _context = self.endpoint_context + _context.cdb["client_2"] = _context.cdb["client_1"] + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["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["client_id"] = "client_2" + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + + _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"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req,) + assert isinstance(_resp, TokenErrorResponse) + assert _resp.to_dict() == { + "error": "invalid_grant", "error_description": "Wrong client" + } class TestOldTokens(object): @pytest.fixture(autouse=True)