From 3731c162c9a25ba2f0548eb2b1e0dfeea9d9a887 Mon Sep 17 00:00:00 2001 From: Nikos Mastoris Date: Wed, 5 Jun 2024 11:30:32 +0000 Subject: [PATCH] Redirect URI comparison for native OAuth clients (RFC6749) --- private/xmetadata/oidc.py | 3 +- src/idpyoidc/client/claims/oidc.py | 3 +- src/idpyoidc/client/defaults.py | 4 +- src/idpyoidc/message/oidc/__init__.py | 7 +- src/idpyoidc/server/exception.py | 2 +- src/idpyoidc/server/oauth2/authorization.py | 171 +++++++++++------- src/idpyoidc/server/oidc/registration.py | 8 +- tests/test_06_oidc.py | 15 +- tests/test_08_transform.py | 5 +- tests/test_09_work_condition.py | 13 +- tests/test_client_02_entity.py | 11 +- tests/test_client_02b_entity_metadata.py | 3 +- .../test_client_14_service_context_impexp.py | 7 +- tests/test_client_21_oidc_service.py | 7 +- tests/test_client_26_read_registration.py | 5 +- tests/test_client_27_conversation.py | 5 +- tests/test_client_30_rp_handler_oidc.py | 5 +- tests/test_client_41_rp_handler_persistent.py | 3 +- ...st_server_23_oidc_registration_endpoint.py | 8 +- ...server_24_oauth2_authorization_endpoint.py | 130 +++++++++++++ .../test_server_32_oidc_read_registration.py | 3 +- 21 files changed, 307 insertions(+), 111 deletions(-) diff --git a/private/xmetadata/oidc.py b/private/xmetadata/oidc.py index 6126e045..9f8db994 100644 --- a/private/xmetadata/oidc.py +++ b/private/xmetadata/oidc.py @@ -4,6 +4,7 @@ from idpyoidc import metadata from idpyoidc.client import metadata as client_metadata +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse @@ -64,7 +65,7 @@ class Metadata(client_metadata.Metadata): _supports = { "acr_values_supported": None, - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "callback_uris": None, # "client_authn_methods": get_client_authn_methods, "client_id": None, diff --git a/src/idpyoidc/client/claims/oidc.py b/src/idpyoidc/client/claims/oidc.py index 7da04a9c..0529f162 100644 --- a/src/idpyoidc/client/claims/oidc.py +++ b/src/idpyoidc/client/claims/oidc.py @@ -5,6 +5,7 @@ from idpyoidc import metadata from idpyoidc.client import claims as client_claims from idpyoidc.client.claims.transform import create_registration_request +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse @@ -63,7 +64,7 @@ class Claims(client_claims.Claims): _supports = { "acr_values_supported": None, - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "callback_uris": None, # "client_authn_methods": get_client_authn_methods, "client_id": None, diff --git a/src/idpyoidc/client/defaults.py b/src/idpyoidc/client/defaults.py index fbacba9b..d7b2a50b 100644 --- a/src/idpyoidc/client/defaults.py +++ b/src/idpyoidc/client/defaults.py @@ -1,6 +1,8 @@ import hashlib import string +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB + SUCCESSFUL = [200, 201, 202, 203, 204, 205, 206] SERVICE_NAME = "OIC" @@ -27,7 +29,7 @@ } DEFAULT_CLIENT_PREFERENCES = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "response_types": [ "code", "id_token", diff --git a/src/idpyoidc/message/oidc/__init__.py b/src/idpyoidc/message/oidc/__init__.py index a6da5063..51e3ee9a 100644 --- a/src/idpyoidc/message/oidc/__init__.py +++ b/src/idpyoidc/message/oidc/__init__.py @@ -47,6 +47,9 @@ NONCE_STORAGE_TIME = 4 * 3600 +APPLICATION_TYPE_NATIVE = "native" +APPLICATION_TYPE_WEB = "web" + class AtHashError(VerificationError): pass @@ -638,9 +641,9 @@ class RegistrationRequest(Message): # "organization_name": SINGLE_OPTIONAL_STRING, "response_modes": OPTIONAL_LIST_OF_STRINGS, } - c_default = {"application_type": "web", "response_types": ["code"]} + c_default = {"application_type": APPLICATION_TYPE_WEB, "response_types": ["code"]} c_allowed_values = { - "application_type": ["native", "web"], + "application_type": [APPLICATION_TYPE_NATIVE, APPLICATION_TYPE_WEB], "subject_type": ["public", "pairwise"], } diff --git a/src/idpyoidc/server/exception.py b/src/idpyoidc/server/exception.py index bfe5a30d..70e4d395 100755 --- a/src/idpyoidc/server/exception.py +++ b/src/idpyoidc/server/exception.py @@ -66,7 +66,7 @@ class UnknownAssertionType(OidcEndpointError): pass -class RedirectURIError(OidcEndpointError): +class RedirectURIError(OidcEndpointError, ValueError): pass diff --git a/src/idpyoidc/server/oauth2/authorization.py b/src/idpyoidc/server/oauth2/authorization.py index 9f7b2723..2dde9b98 100755 --- a/src/idpyoidc/server/oauth2/authorization.py +++ b/src/idpyoidc/server/oauth2/authorization.py @@ -2,7 +2,11 @@ import logging from typing import List from typing import Optional +from typing import TypeVar from typing import Union +from urllib.parse import ParseResult +from urllib.parse import SplitResult +from urllib.parse import parse_qs from urllib.parse import unquote from urllib.parse import urlencode from urllib.parse import urlparse @@ -21,6 +25,8 @@ from idpyoidc.message import Message from idpyoidc.message import oauth2 from idpyoidc.message.oauth2 import AuthorizationRequest +from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import verified_claim_name from idpyoidc.server.authn_event import create_authn_event @@ -41,7 +47,9 @@ from idpyoidc.time_util import utc_time_sans_frac from idpyoidc.util import importer from idpyoidc.util import rndstr -from idpyoidc.util import split_uri + + +ParsedURI = TypeVar('ParsedURI', ParseResult, SplitResult) logger = logging.getLogger(__name__) @@ -106,80 +114,115 @@ def verify_uri( :param context: An EndpointContext instance :param request: The authorization request :param uri_type: redirect_uri or post_logout_redirect_uri - :return: An error response if the redirect URI is faulty otherwise - None + :return: Raise an exception response if the redirect URI is faulty otherwise None """ - _cid = request.get("client_id", client_id) - if not _cid: - logger.error("No client id found") + client_id = request.get("client_id") or client_id + if not client_id: + logger.error("No client_id provided") raise UnknownClient("No client_id provided") - _uri = request.get(uri_type) - if _uri is None: - raise ValueError(f"Wrong uri_type: {uri_type}") + client_info = context.cdb.get(client_id) + if not client_info: + logger.error("No client info found") + raise KeyError("No client info found") - _redirect_uri = unquote(_uri) + req_redirect_uri_quoted = request.get(uri_type) + if req_redirect_uri_quoted is None: + raise ValueError(f"Wrong uri_type: {uri_type}") - part = urlparse(_redirect_uri) - if part.fragment: + req_redirect_uri = unquote(req_redirect_uri_quoted) + req_redirect_uri_obj = urlparse(req_redirect_uri) + if req_redirect_uri_obj.fragment: raise URIError("Contains fragment") - (_base, _query) = split_uri(_redirect_uri) + # basic URL validation + if not req_redirect_uri_obj.hostname: + raise URIError("Invalid redirect_uri hostname") + if req_redirect_uri_obj.path and not req_redirect_uri_obj.path.startswith("/"): + raise URIError("Invalid redirect_uri path") + try: + req_redirect_uri_obj.port + except ValueError as e: + raise URIError(f"Invalid redirect_uri port: {str(e)}") from e + + uri_type_property = f"{uri_type}s" if uri_type == "redirect_uri" else uri_type + client_redirect_uris: list[Union[str, tuple[str, dict]]] = client_info.get(uri_type_property) + if not client_redirect_uris: + # an OIDC client must have registered with redirect URIs + if endpoint_type == "oidc": + raise RedirectURIError(f"No registered {uri_type} for {client_id}") + else: + return + + # TODO move: this processing should be done during client registration/loading + # TODO optimize: keep unique URIs (mayby use a set) + # Pre-processing to homogenize the types of each item, + # and normalize (lower-case, remove params, etc) the rediret URIs. + # Each item is a tuple composed of: + # - a ParseResult item, representing a URI without the query part, and + # - a dict, representing a query string + client_redirect_uris_obj: list[tuple[ParseResult, dict[str, list[str]]]] = [ + ( + urlparse(uri_base)._replace(query=None), + (uri_qs_obj or {}), + ) + for uri in client_redirect_uris + for uri_base, uri_qs_obj in [(uri, {}) if isinstance(uri, str) else uri] + ] + + # Handle redirect URIs for native clients: + # When the URI is an http localhost (IPv4 or IPv6) literal, then + # the port should not be taken into account when matching redirect URIs. + client_type = client_info.get("application_type") or APPLICATION_TYPE_WEB + if client_type == APPLICATION_TYPE_NATIVE: + if is_http_uri(req_redirect_uri_obj) and is_localhost_uri(req_redirect_uri_obj): + req_redirect_uri_obj = remove_port_from_uri(req_redirect_uri_obj) + + # TODO move: this processing should be done during client registration/loading + # When the URI is an http localhost (IPv4 or IPv6) literal, then + # the port should not be taken into account when matching redirect URIs. + _client_redirect_uris_without_port_obj = [] + for uri_obj, url_qs_obj in client_redirect_uris_obj: + if is_http_uri(uri_obj) and is_localhost_uri(uri_obj): + uri_obj = remove_port_from_uri(uri_obj) + _client_redirect_uris_without_port_obj.append((uri_obj, url_qs_obj)) + client_redirect_uris_obj = _client_redirect_uris_without_port_obj + + # Separate the URL from the query string object for the requested redirect URI. + req_redirect_uri_query_obj = parse_qs(req_redirect_uri_obj.query) + req_redirect_uri_without_query_obj = req_redirect_uri_obj._replace(query=None) + + match = any( + req_redirect_uri_without_query_obj == uri_obj + and req_redirect_uri_query_obj == uri_query_obj + for uri_obj, uri_query_obj in client_redirect_uris_obj + ) + if not match: + raise RedirectURIError("Doesn't match any registered uris") + - # Get the clients registered redirect uris - client_info = context.cdb.get(_cid) - if client_info is None: - raise KeyError("No such client") +def is_http_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool: + value = uri_obj.scheme == "http" + return value - if uri_type == "redirect_uri": - redirect_uris = client_info.get(f"{uri_type}s") - else: - redirect_uris = client_info.get(f"{uri_type}") - if redirect_uris is None: - if endpoint_type == "oidc": - raise RedirectURIError(f"No registered {uri_type} for {_cid}") - else: - match = False - for _item in redirect_uris: - if isinstance(_item, str): - regbase = _item - rquery = {} - else: - regbase, rquery = _item - - # The URI MUST exactly match one of the Redirection URI - if _base == regbase: - # every registered query component must exist in the uri - if rquery: - if not _query: - raise ValueError("Missing query part") - - for key, vals in rquery.items(): - if key not in _query: - raise ValueError('"{}" not in query part'.format(key)) - - for val in vals: - if val not in _query[key]: - raise ValueError("{}={} value not in query part".format(key, val)) - - # and vice versa, every query component in the uri - # must be registered - if _query: - if not rquery: - raise ValueError("No registered query part") - - for key, vals in _query.items(): - if key not in rquery: - raise ValueError('"{}" extra in query part'.format(key)) - for val in vals: - if val not in rquery[key]: - raise ValueError("Extra {}={} value in query part".format(key, val)) - match = True - break - if not match: - raise RedirectURIError("Doesn't match any registered uris") +def is_localhost_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool: + value = uri_obj.hostname in [ + "127.0.0.1", + "::1", + "0000:0000:0000:0000:0000:0000:0000:0001", + ] + return value + + +def remove_port_from_uri(uri_obj: ParsedURI) -> ParsedURI: + if not uri_obj.port or not uri_obj.netloc: + return uri_obj + + netloc_without_port = uri_obj.netloc.rsplit(":", 1)[0] + uri_without_port_obj = uri_obj._replace(netloc=netloc_without_port) + return uri_without_port_obj def join_query(base, query): diff --git a/src/idpyoidc/server/oidc/registration.py b/src/idpyoidc/server/oidc/registration.py index 1b8f0bed..a363ebeb 100644 --- a/src/idpyoidc/server/oidc/registration.py +++ b/src/idpyoidc/server/oidc/registration.py @@ -13,6 +13,8 @@ from idpyoidc.exception import MessageException from idpyoidc.message.oauth2 import ResponseMessage +from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import ClientRegistrationErrorResponse from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse @@ -291,10 +293,10 @@ def do_client_registration(self, request, client_id, ignore=None): @staticmethod def verify_redirect_uris(registration_request): verified_redirect_uris = [] - client_type = registration_request.get("application_type", "web") + client_type = registration_request.get("application_type") or APPLICATION_TYPE_WEB must_https = False - if client_type == "web": + if client_type == APPLICATION_TYPE_WEB: must_https = True if registration_request.get("response_types") == ["code"]: must_https = False @@ -302,7 +304,7 @@ def verify_redirect_uris(registration_request): for uri in registration_request["redirect_uris"]: _custom = False p = urlparse(uri) - if client_type == "native": + if client_type == APPLICATION_TYPE_NATIVE: if p.scheme not in ["http", "https"]: # Custom scheme _custom = True elif p.scheme == "http" and p.hostname in ["localhost", "127.0.0.1"]: diff --git a/tests/test_06_oidc.py b/tests/test_06_oidc.py index 09b3b256..4c4c9fa8 100644 --- a/tests/test_06_oidc.py +++ b/tests/test_06_oidc.py @@ -27,6 +27,7 @@ from idpyoidc.message.oidc import AccessTokenRequest from idpyoidc.message.oidc import AccessTokenResponse from idpyoidc.message.oidc import AddressClaim +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AtHashError from idpyoidc.message.oidc import AuthnToken from idpyoidc.message.oidc import AuthorizationErrorResponse @@ -520,7 +521,7 @@ def test_token_endpoint_is_required_for_other_than_implicit_flow_only(self): class TestRegistrationRequest(object): def test_deserialize(self): msg = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -550,7 +551,7 @@ def test_registration_request(self): default_max_age=10, require_auth_time=True, default_acr="foo", - application_type="web", + application_type=APPLICATION_TYPE_WEB, redirect_uris=["https://example.com/authz_cb"], ) assert req.verify() @@ -558,7 +559,7 @@ def test_registration_request(self): js_obj = json.loads(js) expected_js_obj = { "redirect_uris": ["https://example.com/authz_cb"], - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "default_acr": "foo", "require_auth_time": True, "operation": "register", @@ -595,7 +596,7 @@ def test_deser(self): default_max_age=10, require_auth_time=True, default_acr="foo", - application_type="web", + application_type=APPLICATION_TYPE_WEB, redirect_uris=["https://example.com/authz_cb"], ) ser_req = req.serialize("urlencoded") @@ -616,7 +617,7 @@ def test_deser_dict(self): "default_max_age": 10, "require_auth_time": True, "default_acr": "foo", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": ["https://example.com/authz_cb"], } @@ -637,7 +638,7 @@ def test_deser_dict_json(self): "default_max_age": 10, "require_auth_time": True, "default_acr": "foo", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": ["https://example.com/authz_cb"], } @@ -663,7 +664,7 @@ def test_deserialize(self): "registration_client_uri": "https://server.example.com/connect/register?client_id" "=s6BhdRkqt3", "token_endpoint_auth_method": "client_secret_basic", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", diff --git a/tests/test_08_transform.py b/tests/test_08_transform.py index 8afe9095..71c83d9b 100644 --- a/tests/test_08_transform.py +++ b/tests/test_08_transform.py @@ -7,6 +7,7 @@ from idpyoidc.client.claims.transform import create_registration_request from idpyoidc.client.claims.transform import preferred_to_registered from idpyoidc.client.claims.transform import supported_to_preferred +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import ProviderConfigurationResponse from idpyoidc.message.oidc import RegistrationRequest @@ -307,7 +308,7 @@ def setup(self): self.supported = supported preference = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -376,7 +377,7 @@ def test_registration_response(self): assert registration_request["subject_type"] == "public" registration_response = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", diff --git a/tests/test_09_work_condition.py b/tests/test_09_work_condition.py index 4cfa075f..957d8570 100644 --- a/tests/test_09_work_condition.py +++ b/tests/test_09_work_condition.py @@ -7,6 +7,7 @@ from idpyoidc.client.claims.transform import create_registration_request from idpyoidc.client.claims.transform import preferred_to_registered from idpyoidc.client.claims.transform import supported_to_preferred +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB KEYSPEC = [ {"type": "RSA", "use": ["sig"]}, @@ -46,7 +47,7 @@ def setup(self): def test_load_conf(self): # Only symmetric key client_conf = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -65,7 +66,7 @@ def test_load_conf(self): def test_load_jwks(self): # Symmetric and asymmetric keys published as JWKS client_conf = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "base_url": "https://client.example.org/", "redirect_uris": [ "https://client.example.org/callback", @@ -86,7 +87,7 @@ def test_load_jwks(self): def test_load_jwks_uri1(self): # Symmetric and asymmetric keys published through a jwks_uri client_conf = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "base_url": "https://client.example.org/", "redirect_uris": [ "https://client.example.org/callback", @@ -108,7 +109,7 @@ def test_load_jwks_uri1(self): def test_load_jwks_uri2(self): # Symmetric and asymmetric keys published through a jwks_uri client_conf = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "base_url": "https://client.example.org/", "redirect_uris": [ "https://client.example.org/callback", @@ -127,7 +128,7 @@ def test_load_jwks_uri2(self): def test_registration_response(self): client_conf = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "base_url": "https://client.example.org/", "redirect_uris": [ "https://client.example.org/callback", @@ -197,7 +198,7 @@ def test_registration_response(self): assert registration_request["subject_type"] == "public" registration_response = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", diff --git a/tests/test_client_02_entity.py b/tests/test_client_02_entity.py index f8929b5c..369ef7fc 100644 --- a/tests/test_client_02_entity.py +++ b/tests/test_client_02_entity.py @@ -2,6 +2,7 @@ from idpyoidc.client.client_auth import ClientAuthnMethod from idpyoidc.client.entity import Entity +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, @@ -61,7 +62,7 @@ def test_get_service_context(self): def test_client_authn_default(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "keys": {"key_defs": KEYSPEC, "read_only": True}, @@ -74,7 +75,7 @@ def test_client_authn_default(): def test_client_authn_by_names(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "keys": {"key_defs": KEYSPEC, "read_only": True}, @@ -99,7 +100,7 @@ def modify_request(self, request, service, **kwargs): def test_client_authn_full(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "keys": {"key_defs": KEYSPEC, "read_only": True}, @@ -121,7 +122,7 @@ def test_client_authn_full(): def test_service_specific(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "keys": {"key_defs": KEYSPEC, "read_only": True}, @@ -150,7 +151,7 @@ def test_service_specific(): def test_service_specific2(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "keys": {"key_defs": KEYSPEC, "read_only": True}, diff --git a/tests/test_client_02b_entity_metadata.py b/tests/test_client_02b_entity_metadata.py index fd542125..8f122a56 100644 --- a/tests/test_client_02b_entity_metadata.py +++ b/tests/test_client_02b_entity_metadata.py @@ -1,6 +1,7 @@ from cryptojwt.key_jar import init_key_jar from idpyoidc.client.entity import Entity +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationRequest ISS = "http://example.org/op" @@ -12,7 +13,7 @@ "issuer": ISS, "application_name": "rphandler", "preference": { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": "support@example.com", "response_types_supported": ["code"], "request_parameter": "request_uri", diff --git a/tests/test_client_14_service_context_impexp.py b/tests/test_client_14_service_context_impexp.py index 1a994cd9..78af86b2 100644 --- a/tests/test_client_14_service_context_impexp.py +++ b/tests/test_client_14_service_context_impexp.py @@ -7,6 +7,7 @@ from idpyoidc.client.entity import Entity from idpyoidc.client.service_context import ServiceContext +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB BASE_URL = "https://example.com" @@ -111,7 +112,7 @@ def create_client_info_instance(self): def test_registration_userinfo_sign_enc_algs(self): self.service_context.claims.use = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -130,7 +131,7 @@ def test_registration_userinfo_sign_enc_algs(self): def test_registration_request_object_sign_enc_algs(self): self.service_context.claims.use = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -152,7 +153,7 @@ def test_registration_request_object_sign_enc_algs(self): def test_registration_id_token_sign_enc_algs(self): self.service_context.claims.use = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", diff --git a/tests/test_client_21_oidc_service.py b/tests/test_client_21_oidc_service.py index 028561f9..5eba310b 100644 --- a/tests/test_client_21_oidc_service.py +++ b/tests/test_client_21_oidc_service.py @@ -16,6 +16,7 @@ from idpyoidc.exception import MissingRequiredAttribute from idpyoidc.message.oidc import AccessTokenRequest from idpyoidc.message.oidc import AccessTokenResponse +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AuthorizationRequest from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import IdToken @@ -483,7 +484,7 @@ def create_service(self): "redirect_uris": ["https://example.com/cli/authz_cb"], "issuer": self._iss, "application_name": "rphandler", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "preference": { "scope": ["openid", "profile", "email", "address", "phone"], @@ -744,7 +745,7 @@ def test_post_parse(self): del use_copy["callback_uris"] assert use_copy == { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "backchannel_logout_session_required": True, "backchannel_logout_uri": "https://rp.example.com/back", "client_id": "client_id", @@ -831,7 +832,7 @@ def test_post_parse_2(self): del use_copy["callback_uris"] assert use_copy == { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "backchannel_logout_session_required": True, "backchannel_logout_uri": "https://rp.example.com/back", "client_id": "client_id", diff --git a/tests/test_client_26_read_registration.py b/tests/test_client_26_read_registration.py index 20cb8e06..3af61799 100644 --- a/tests/test_client_26_read_registration.py +++ b/tests/test_client_26_read_registration.py @@ -7,6 +7,7 @@ import requests from idpyoidc.client.entity import Entity +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationResponse ISS = "https://example.com" @@ -21,7 +22,7 @@ def create_request(self): "issuer": self._iss, "requests_dir": "requests", "base_url": "https://example.com/cli/", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "response_types_supported": ["code"], "contacts": ["ops@example.org"], "jwks_uri": "https://example.com/rp/static/jwks.json", @@ -61,7 +62,7 @@ def test_construct(self): "registration_client_uri": "{}/registration_api?client_id=zls2qhN1jO6A".format(ISS), "client_secret_expires_at": now + 3600, "client_id_issued_at": now, - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "response_types": ["code"], "contacts": ["ops@example.com"], "redirect_uris": ["{}/authz_cb".format(RP_BASEURL)], diff --git a/tests/test_client_27_conversation.py b/tests/test_client_27_conversation.py index 2f36cca9..fbb22399 100644 --- a/tests/test_client_27_conversation.py +++ b/tests/test_client_27_conversation.py @@ -11,6 +11,7 @@ from idpyoidc.client.oidc.webfinger import WebFinger from idpyoidc.message.oidc import JRD from idpyoidc.message.oidc import AccessTokenResponse +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import Link from idpyoidc.message.oidc import OpenIDSchema @@ -116,7 +117,7 @@ def test_conversation(): config = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.org"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], "response_types": ["code"], @@ -429,7 +430,7 @@ def test_conversation(): "registration_client_uri": f"{RP_BASEURL}/registration?client_id=zls2qhN1jO6A", "client_secret_expires_at": now + 3600, "client_id_issued_at": now, - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "response_types": ["code"], "contacts": ["ops@example.com"], "redirect_uris": [f"{RP_BASEURL}/authz_cb"], diff --git a/tests/test_client_30_rp_handler_oidc.py b/tests/test_client_30_rp_handler_oidc.py index 855916b9..3a3d75f9 100644 --- a/tests/test_client_30_rp_handler_oidc.py +++ b/tests/test_client_30_rp_handler_oidc.py @@ -11,6 +11,7 @@ from idpyoidc.client.entity import Entity from idpyoidc.client.rp_handler import RPHandler from idpyoidc.message.oidc import AccessTokenResponse +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import IdToken from idpyoidc.message.oidc import JRD @@ -23,7 +24,7 @@ BASE_URL = "https://example.com/rp" PREF = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.com"], "response_types_supported": [ "code", @@ -1019,7 +1020,7 @@ def test_dynamic_setup(self): } pcr = ProviderConfigurationResponse(**resp) _crr = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "response_types": ["code", "code id_token"], "redirect_uris": [ "https://example.com/rp/authz_cb" diff --git a/tests/test_client_41_rp_handler_persistent.py b/tests/test_client_41_rp_handler_persistent.py index 113a3dea..8edce035 100644 --- a/tests/test_client_41_rp_handler_persistent.py +++ b/tests/test_client_41_rp_handler_persistent.py @@ -7,13 +7,14 @@ from idpyoidc.client.rp_handler import RPHandler from idpyoidc.message.oidc import AccessTokenResponse +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import AuthorizationResponse from idpyoidc.message.oidc import IdToken BASE_URL = "https://example.com/rp" PREFERENCE = { - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "contacts": ["ops@example.com"], "response_types": [ "code", diff --git a/tests/test_server_23_oidc_registration_endpoint.py b/tests/test_server_23_oidc_registration_endpoint.py index 0b33c570..6f55ccfe 100755 --- a/tests/test_server_23_oidc_registration_endpoint.py +++ b/tests/test_server_23_oidc_registration_endpoint.py @@ -6,6 +6,8 @@ import responses from cryptojwt.key_jar import init_key_jar +from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.message.oidc import RegistrationResponse from idpyoidc.server import Server @@ -48,7 +50,7 @@ MSG = { "client_id": "client_id", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2", @@ -269,7 +271,7 @@ def test_register_custom_redirect_uri_web(self): def test_register_custom_redirect_uri_native(self): _msg = MSG.copy() _msg["redirect_uris"] = ["custom://cb.example.com"] - _msg["application_type"] = "native" + _msg["application_type"] = APPLICATION_TYPE_NATIVE _req = self.endpoint.parse_request(RegistrationRequest(**_msg).to_json()) with responses.RequestsMock() as rsps: rsps.add( @@ -287,7 +289,7 @@ def test_sector_uri_missing_redirect_uri(self): _msg = MSG.copy() _msg["redirect_uris"] = ["custom://cb.example.com"] - _msg["application_type"] = "native" + _msg["application_type"] = APPLICATION_TYPE_NATIVE _msg["sector_identifier_uri"] = _url _req = self.endpoint.parse_request(RegistrationRequest(**_msg).to_json()) diff --git a/tests/test_server_24_oauth2_authorization_endpoint.py b/tests/test_server_24_oauth2_authorization_endpoint.py index a6623fbd..f6522a77 100755 --- a/tests/test_server_24_oauth2_authorization_endpoint.py +++ b/tests/test_server_24_oauth2_authorization_endpoint.py @@ -18,6 +18,8 @@ from idpyoidc.message.oauth2 import AuthorizationErrorResponse from idpyoidc.message.oauth2 import AuthorizationRequest from idpyoidc.message.oauth2 import AuthorizationResponse +from idpyoidc.message.oidc import APPLICATION_TYPE_NATIVE +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.server import Server from idpyoidc.server.authn_event import create_authn_event from idpyoidc.server.authz import AuthzHandling @@ -358,6 +360,134 @@ def test_verify_uri_unregistered(self): with pytest.raises(RedirectURIError): verify_uri(_context, request, "redirect_uri", "client_id") + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://127.0.0.1:9999/auth_cb", "http://127.0.0.1/auth_cb"), + ("http://127.0.0.1:9999/auth_cb", "http://127.0.0.1:3456/auth_cb"), + ("http://127.0.0.1/auth_cb", "http://127.0.0.1/auth_cb"), + ("http://127.0.0.1/auth_cb", "http://127.0.0.1:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv4_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://[::1]:9999/auth_cb", "http://[::1]/auth_cb"), + ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv6_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://localhost:9999/auth_cb", "http://localhost/auth_cb"), + ("http://localhost:9999/auth_cb", "http://localhost:3456/auth_cb"), + ("http://localhost/auth_cb", "http://localhost:3456/auth_cb"), + ] + ) + def test_verify_uri_literal_localhost_native_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://127.0.0.1:9999/auth_cb", "http://127.0.0.1/auth_cb"), + ("http://127.0.0.1:9999/auth_cb", "http://127.0.0.1:3456/auth_cb"), + ("http://127.0.0.1/auth_cb", "http://127.0.0.1:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv4_web_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, redirect_uri", [ + ("http://[::1]:9999/auth_cb", "http://[::1]/auth_cb"), + ("http://[::1]:9999/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[::1]/auth_cb", "http://[::1]:3456/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb"), + ] + ) + def test_verify_uri_localhost_ipv6_web_client(self, client_redirect_uri, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, {})],"application_type": APPLICATION_TYPE_WEB} + request = {"redirect_uri": redirect_uri} + with pytest.raises(RedirectURIError): + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ + ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:9999/auth_cb?foo=bar"), + ("http://127.0.0.1:9999/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), + ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1/auth_cb?foo=bar"), + ("http://127.0.0.1/auth_cb", {"foo":["bar"]}, "http://127.0.0.1:3456/auth_cb?foo=bar"), + ] + ) + def test_verify_uri_qp_localhost_ipv4_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)],"application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ + ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:9999/auth_cb?foo=bar"), + ("http://[::1]:9999/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), + ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]/auth_cb?foo=bar"), + ("http://[::1]/auth_cb", {"foo":["bar"]}, "http://[::1]:3456/auth_cb?foo=bar"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]:9999/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]:3456/auth_cb?foo=bar"), + ("http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb", {"foo":["bar"]}, "http://[0000:0000:0000:0000:0000:0000:0000:0001]/auth_cb?foo=bar"), + ] + ) + def test_verify_uri_qp_localhost_ipv6_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} + request = {"redirect_uri": redirect_uri} + + verify_uri(_context, request, "redirect_uri", "client_id") + + @pytest.mark.parametrize( + "client_redirect_uri, client_redirect_uri_qp, redirect_uri", [ + ("https://rp.example.com:9999/auth_cb", {"foo":["bar"]}, "http://rp.example.com/auth_cb?foo=bar"), + ("https://rp.example.com/auth_cb", {"foo":["bar"]}, "http://rp.example.com:9999/auth_cb?foo=bar"), + ] + ) + def test_verify_uri_qp_match_native_client(self, client_redirect_uri, client_redirect_uri_qp, redirect_uri): + _context = self.endpoint.upstream_get("context") + _context.cdb["client_id"] = {"redirect_uris": [(client_redirect_uri, client_redirect_uri_qp)], "application_type": APPLICATION_TYPE_NATIVE} + + request = {"redirect_uri": redirect_uri} + + with pytest.raises(ValueError): + verify_uri(_context, request, "redirect_uri", "client_id") + def test_verify_uri_qp_match(self): _context = self.endpoint.upstream_get("context") _context.cdb["client_id"] = { diff --git a/tests/test_server_32_oidc_read_registration.py b/tests/test_server_32_oidc_read_registration.py index bac0e207..e09bc5cd 100644 --- a/tests/test_server_32_oidc_read_registration.py +++ b/tests/test_server_32_oidc_read_registration.py @@ -4,6 +4,7 @@ import pytest +from idpyoidc.message.oidc import APPLICATION_TYPE_WEB from idpyoidc.message.oidc import RegistrationRequest from idpyoidc.server import Server from idpyoidc.server.configure import OPConfiguration @@ -51,7 +52,7 @@ msg = { "client_id": "client_1", - "application_type": "web", + "application_type": APPLICATION_TYPE_WEB, "redirect_uris": [ "https://client.example.org/callback", "https://client.example.org/callback2",