Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@ pytest-isort>=1.3.0
pytest-localserver>=0.5.0
flake8
bandit
urllib3<1.27
urllib3<1.27
cryptojwt>=1.8.4
pyOpenSSL
filelock>=3.0.12
pyyaml>=5.1.2
jinja2>=2.11.3
responses>=0.13.0
1 change: 1 addition & 0 deletions src/idpyoidc/message/oauth2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class AccessTokenRequest(Message):
"client_id": SINGLE_OPTIONAL_STRING,
"client_secret": SINGLE_OPTIONAL_STRING,
"state": SINGLE_OPTIONAL_STRING,
"resource": OPTIONAL_LIST_OF_STRINGS,
}
c_default = {"grant_type": "authorization_code"}

Expand Down
5 changes: 3 additions & 2 deletions src/idpyoidc/server/authz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ def __call__(
else:
setattr(grant, key, val)

if resources is None:
# Grant.resources may already has value
if grant.resources is None and resources is None:
grant.resources = [_client_id]
else:
elif resources is not None:
grant.resources = resources

# Scope handling. If allowed scopes are defined for the client filter using that
Expand Down
9 changes: 4 additions & 5 deletions src/idpyoidc/server/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,10 @@ def client_authentication(self, request: Message, http_info: Optional[dict] = No
authn_info = verify_client(request=request, http_info=http_info, **kwargs)

LOGGER.debug("authn_info: %s", authn_info)
if authn_info == {}:
if self.client_authn_method and len(self.client_authn_method):
LOGGER.debug("client_authn_method: %s", self.client_authn_method)
raise UnAuthorizedClient("Authorization failed")
elif "client_id" not in authn_info and authn_info.get("method") != "none":
if authn_info == {} and self.client_authn_method and len(self.client_authn_method):
LOGGER.debug("client_authn_method: %s", self.client_authn_method)
raise UnAuthorizedClient("Authorization failed")
if "client_id" not in authn_info and authn_info.get("method") != "none":
raise UnAuthorizedClient("Authorization failed")
return authn_info

Expand Down
124 changes: 62 additions & 62 deletions src/idpyoidc/server/oauth2/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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 AuthorizationResponse, TokenErrorResponse
from idpyoidc.message.oidc import verified_claim_name
from idpyoidc.server.authn_event import create_authn_event
from idpyoidc.server.cookie_handler import compute_session_state
Expand All @@ -41,6 +41,8 @@
from idpyoidc.server.exception import ToOld
from idpyoidc.server.exception import UnAuthorizedClientScope
from idpyoidc.server.exception import UnknownClient
from idpyoidc.server.oauth2.token_helper import apply_audience_policies
from idpyoidc.server.oauth2.token_helper import validate_resource_indicators_policy
from idpyoidc.server.session import Revoked
from idpyoidc.server.token.exception import UnknownToken
from idpyoidc.server.user_authn.authn_context import pick_auth
Expand Down Expand Up @@ -103,7 +105,8 @@ def verify_uri(
request: Union[dict, Message],
uri_type: str,
client_id: Optional[str] = None,
endpoint_type: Optional[str] = 'oidc'
endpoint_type: Optional[str] = 'oidc',
schemes_denylist: Optional[list] = None
):
"""
A redirect URI
Expand Down Expand Up @@ -175,6 +178,8 @@ def verify_uri(
# 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 not has_uri_allowed_scheme(req_redirect_uri_obj, schemes_denylist or []):
raise URIError("Invalid scheme in redirect URI")
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)

Expand Down Expand Up @@ -205,6 +210,9 @@ def is_http_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool:
value = uri_obj.scheme == "http"
return value

def has_uri_allowed_scheme(uri_obj: Union[ParseResult, SplitResult], schemes_denylist) -> bool:
value = uri_obj.scheme not in schemes_denylist
return value

def is_localhost_uri(uri_obj: Union[ParseResult, SplitResult]) -> bool:
value = uri_obj.hostname in [
Expand Down Expand Up @@ -240,7 +248,8 @@ def join_query(base, query):
def get_uri(context,
request: Union[Message, dict],
uri_type: str,
endpoint_type: Optional[str] = "oidc"):
endpoint_type: Optional[str] = "oidc",
schemes_denylist: Optional[list] = None):
"""verify that the redirect URI is reasonable.

:param context: An EndpointContext instance
Expand All @@ -251,7 +260,7 @@ def get_uri(context,
uri = ""

if uri_type in request:
verify_uri(context, request, uri_type, endpoint_type=endpoint_type)
verify_uri(context, request, uri_type, endpoint_type=endpoint_type, schemes_denylist=schemes_denylist)
uri = request[uri_type]
else:
uris = f"{uri_type}s"
Expand Down Expand Up @@ -331,53 +340,6 @@ def check_unknown_scopes_policy(request_info, client_id, context):
raise UnAuthorizedClientScope()


def validate_resource_indicators_policy(request, context, **kwargs):
if "resource" not in request:
return request

resource_servers_per_client = kwargs["resource_servers_per_client"]
client_id = request["client_id"]

if (
isinstance(resource_servers_per_client, dict)
and client_id not in resource_servers_per_client
):
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Resources for client {client_id} not found",
)

if isinstance(resource_servers_per_client, dict):
permitted_resources = [res for res in resource_servers_per_client[client_id]]
else:
permitted_resources = [res for res in resource_servers_per_client]

common_resources = list(set(request["resource"]).intersection(set(permitted_resources)))
if not common_resources:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

common_resources = [r for r in common_resources if r in context.cdb.keys()]
if not common_resources:
return oauth2.AuthorizationErrorResponse(
error="invalid_target",
error_description=f"Invalid resource requested by client {client_id}",
)

if client_id not in common_resources:
common_resources.append(client_id)

request["resource"] = common_resources

permitted_scopes = [context.cdb[r]["allowed_scopes"] for r in common_resources]
permitted_scopes = [r for res in permitted_scopes for r in res]
scopes = list(set(request.get("scope", [])).intersection(set(permitted_scopes)))
request["scope"] = scopes
return request


class Authorization(Endpoint):
request_cls = oauth2.AuthorizationRequest
response_cls = oauth2.AuthorizationResponse
Expand Down Expand Up @@ -412,6 +374,7 @@ def __init__(self, upstream_get, **kwargs):
self.post_parse_request.append(self._post_parse_request)
self.allowed_request_algorithms = AllowedAlgorithms(ALG_PARAMS)
self.resource_indicators_config = kwargs.get("resource_indicators", None)
self.schemes_denylist = kwargs.get("schemes_denylist", None)

def filter_request(self, context, req):
return req
Expand Down Expand Up @@ -556,7 +519,7 @@ def _post_parse_request(self, request, client_id, context, **kwargs):

# Get a verified redirect URI
try:
redirect_uri = get_uri(context, request, "redirect_uri", self.endpoint_type)
redirect_uri = get_uri(context, request, "redirect_uri", self.endpoint_type, self.schemes_denylist)
except (RedirectURIError, ParameterError, URIError, UnknownClient) as err:
return self.authentication_error_response(
request,
Expand All @@ -566,19 +529,50 @@ def _post_parse_request(self, request, client_id, context, **kwargs):
else:
request["redirect_uri"] = redirect_uri

if (
"resource_indicators" in _cinfo
and "authorization_code" in _cinfo["resource_indicators"]
):
resource_indicators_config = _cinfo["resource_indicators"]["authorization_code"]
else:
resource_indicators_config = self.resource_indicators_config
resource_indicators_config = None
# check if enable_resource_indicators is enabled and resource parameter exists
if request.get("resource") is not None and context.conf.endpoint.get("authorization").get("kwargs").get("enable_resource_indicators"):
if "resource_indicators" in _cinfo:
resource_indicators_config = _cinfo["resource_indicators"]
if client_id in request.get("resource"):
if resource_indicators_config == None:
resource_indicators_config = {
"policy": {
"function": validate_resource_indicators_policy,
"kwargs": {
"resource_servers_per_client": [
client_id
]
}
}
}
else:
# Ensure the structure exists
if "policy" in resource_indicators_config and "kwargs" in resource_indicators_config["policy"]:
resource_indicators_config["policy"]["kwargs"].setdefault("resource_servers_per_client", []).append(client_id)
else:
# If the structure is somehow not complete, initialize it
resource_indicators_config["policy"] = {
"function": validate_resource_indicators_policy,
"kwargs": {
"resource_servers_per_client": [
client_id
]
}
}

if resource_indicators_config is not None:
if "policy" not in resource_indicators_config:
policy = {"policy": {"function": validate_resource_indicators_policy}}
resource_indicators_config.update(policy)
request = self._enforce_resource_indicators_policy(request, resource_indicators_config)

if "error" in request:
return self.authentication_error_response(
request,
error=request["error"],
error_description=request["error_description"],
)

return request

Expand All @@ -589,9 +583,6 @@ def _enforce_resource_indicators_policy(self, request, config):
function = policy["function"]
kwargs = policy.get("kwargs", {})

if kwargs.get("resource_servers_per_client", None) is None:
kwargs["resource_servers_per_client"] = {request["client_id"]: request["client_id"]}

if isinstance(function, str):
try:
fn = importer(function)
Expand Down Expand Up @@ -931,6 +922,15 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict
else:
_aud_arg = {}

client_id = request["client_id"]
_cinfo = _context.cdb.get(client_id)
apply_audience_policies(request, _context, _cinfo, request.get("resource", None), grant, _context.conf.endpoint.get("authorization").get("kwargs"))
if "error" in request:
return self.authentication_error_response(
request,
error=request["error"],
error_description=request["error_description"],
)
if "code" in rtype:
_code = self.mint_token(
token_class="authorization_code",
Expand Down
20 changes: 16 additions & 4 deletions src/idpyoidc/server/oauth2/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from idpyoidc.message import oauth2
from idpyoidc.server.endpoint import Endpoint
from idpyoidc.server.exception import ToOld
from idpyoidc.server.oauth2.token_helper import apply_audience_policies
from idpyoidc.server.token.exception import UnknownToken
from idpyoidc.server.token.exception import WrongTokenClass

Expand Down Expand Up @@ -34,6 +35,8 @@ def __init__(self, upstream_get, **kwargs):
Endpoint.__init__(self, upstream_get, **kwargs)
self.offset = kwargs.get("offset", 0)
self.enforce_aud_restriction = kwargs.get("enforce_audience_restriction", True)
self.audience_policies_config = kwargs.get("audience_policies", None)
self.enable_audience_policies = kwargs.get("enable_audience_policies", False)

def _introspect(self, token, client_id, grant):
# Make sure that the token is an access_token or a refresh_token
Expand Down Expand Up @@ -117,20 +120,25 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs
aud = grant.resources

client_id = request["client_id"]

apply_audience_policies(request, _context, _context.cdb[client_id], aud, _session_info["grant"], self.kwargs)
if "error" in request:
return {"response_args": _resp}

try:
_cinfo = _context.cdb[client_id]
enforce_aud_restriction = _cinfo.get(
"enforce_audience_restriction", self.enforce_aud_restriction
)
except:
enforce_aud_restriction = self.enforce_aud_restriction
if enforce_aud_restriction:
if request["client_id"] not in aud:
return {"response_args": _resp}

_info = self._introspect(_token, _session_info["client_id"], _session_info["grant"])
_info = self._introspect(_token, _session_info["client_id"], grant)
if _info is None:
return {"response_args": _resp}
if enforce_aud_restriction:
if request["client_id"] not in aud and request["client_id"] not in _info["client_id"]:
return {"response_args": _resp}

if release:
if "username" in release:
Expand All @@ -142,6 +150,10 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs
_resp.update(_info)
_resp.weed()

_custom_attributes = grant.claims.get("custom_attributes")
if _custom_attributes:
_resp.update(_custom_attributes)

_claims_restriction = _context.claims_interface.get_claims(
_session_info["branch_id"], scopes=_token.scope, claims_release_point="introspection"
)
Expand Down
3 changes: 3 additions & 0 deletions src/idpyoidc/server/oauth2/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def __init__(self, upstream_get, new_refresh_token=False, **kwargs):
# list(self.grant_type_helper.keys()))
self.revoke_refresh_on_issue = kwargs.get("revoke_refresh_on_issue", False)
self.resource_indicators_config = kwargs.get("resource_indicators", None)
self.enable_resource_indicators = kwargs.get("enable_resource_indicators", False)
self.audience_policies_config = kwargs.get("audience_policies", None)
self.enable_audience_policies = kwargs.get("enable_audience_policies", False)

def configure_types(self, helpers, default_helpers):
if helpers is None:
Expand Down
Loading
Loading