Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upstream get #99

Merged
merged 8 commits into from
Apr 26, 2024
Merged
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
2 changes: 1 addition & 1 deletion src/idpyoidc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__author__ = "Roland Hedberg"
__version__ = "4.0.0"
__version__ = "4.1.0"

VERIFIED_CLAIM_PREFIX = "__verified"

Expand Down
46 changes: 46 additions & 0 deletions src/idpyoidc/client/claims/oauth2resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Optional

from idpyoidc.client import claims
from idpyoidc.message.oauth2 import OAuthProtectedResourceRequest
from idpyoidc.client.claims.transform import array_or_singleton

class Claims(claims.Claims):
_supports = {
"resource": None,
"grant_types_supported": ["authorization_code", "implicit", "refresh_token"],
"scopes_supported": [],
"authorization_servers": [],
"bearer_methods_supported": [],
"resource_documentation": None,
"resource_signing_alg_values_supported": [],
"resource_encryption_alg_values_supported": [],
"resource_encryption_enc_values_supported": [],
"client_registration_types": [],
"organization_name": None,
"resource_policy_uri": None,
"resource_tos_uri": None
}

callback_path = {}

callback_uris = ["redirect_uris"]

def __init__(self, prefer: Optional[dict] = None, callback_path: Optional[dict] = None):
claims.Claims.__init__(self, prefer=prefer, callback_path=callback_path)

def create_registration_request(self):
_request = {}
for key, spec in OAuthProtectedResourceRequest.c_param.items():
_pref_key = key
if _pref_key in self.prefer:
value = self.prefer[_pref_key]
elif _pref_key in self.supports():
value = self.supports()[_pref_key]
else:
continue

if not value:
continue

_request[key] = array_or_singleton(spec, value)
return _request
13 changes: 9 additions & 4 deletions src/idpyoidc/client/claims/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,16 @@ def supported_to_preferred(
_pref_val = preference.get(key) # defined in configuration
_info_val = info.get(key)
if _info_val:
# Only use provider setting if less or equal to what I support
if key.endswith("supported"): # list
preference[key] = [x for x in _pref_val if x in _info_val]
if isinstance(_info_val, bool):
if _info_val is False and _pref_val is True:
# Turn off support if server doesn't support
preference[key] = _info_val
else:
pass
# Only use provider setting if less or equal to what I support
if key.endswith("supported"): # list
preference[key] = [x for x in _pref_val if x in _info_val]
else:
pass
elif val is None: # No default, means the RP does not have a preference
# if key not in ['jwks_uri', 'jwks']:
pass
Expand Down
4 changes: 1 addition & 3 deletions src/idpyoidc/client/client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,7 @@ def _get_audience_and_algorithm(self, context, keyjar, **kwargs):

def _construct_client_assertion(self, service, **kwargs):
_context = service.upstream_get("context")
_entity = service.upstream_get("entity")
if _entity is None:
_entity = service.upstream_get("unit")
_entity = service.upstream_get("unit")

_keyjar = service.upstream_get("attribute", "keyjar")
audience, algorithm = self._get_audience_and_algorithm(_context, _keyjar, **kwargs)
Expand Down
5 changes: 2 additions & 3 deletions src/idpyoidc/client/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ def __init__(
)

self.setup_client_authn_methods(config)

self.upstream_get = upstream_get

def get_services(self, *arg):
Expand All @@ -170,8 +169,8 @@ def get_service_by_endpoint_name(self, endpoint_name, *arg):

return None

def get_entity(self):
return self
# def get_entity(self):
# return self

def get_client_id(self):
_val = self.context.claims.get_usage("client_id")
Expand Down
72 changes: 62 additions & 10 deletions src/idpyoidc/client/oauth2/add_on/dpop.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@
from typing import Optional

from cryptojwt.jwk.jwk import key_from_jwk_dict
from cryptojwt.jws.jws import JWS
from cryptojwt.jws.jws import factory
from cryptojwt.jws.jws import JWS
from cryptojwt.key_bundle import key_by_alg

from idpyoidc.client.client_auth import BearerHeader
from idpyoidc.client.client_auth import find_token_info
from idpyoidc.client.service_context import ServiceContext
from idpyoidc.message import Message
from idpyoidc.message import SINGLE_OPTIONAL_STRING
from idpyoidc.message import SINGLE_REQUIRED_INT
from idpyoidc.message import SINGLE_REQUIRED_JSON
from idpyoidc.message import SINGLE_REQUIRED_STRING
from idpyoidc.message import Message
from idpyoidc.metadata import get_signing_algs
from idpyoidc.time_util import utc_time_sans_frac

Expand Down Expand Up @@ -91,13 +93,13 @@ def verify_header(self, dpop_header) -> Optional["DPoPProof"]:


def dpop_header(
service_context: ServiceContext,
service_endpoint: str,
http_method: str,
headers: Optional[dict] = None,
token: Optional[str] = "",
nonce: Optional[str] = "",
**kwargs
service_context: ServiceContext,
service_endpoint: str,
http_method: str,
headers: Optional[dict] = None,
token: Optional[str] = "",
nonce: Optional[str] = "",
**kwargs
) -> dict:
"""

Expand Down Expand Up @@ -159,7 +161,7 @@ def dpop_header(
return headers


def add_support(services, dpop_signing_alg_values_supported):
def add_support(services, dpop_signing_alg_values_supported, with_dpop_header=None):
"""
Add the necessary pieces to make pushed authorization happen.

Expand All @@ -185,3 +187,53 @@ def add_support(services, dpop_signing_alg_values_supported):
_userinfo_service = services.get("userinfo")
if _userinfo_service:
_userinfo_service.construct_extra_headers.append(dpop_header)
# To be backward compatible
if with_dpop_header is None:
with_dpop_header = ["userinfo"]

# Add dpop HTTP header to these
for _srv in with_dpop_header:
if _srv == "accesstoken":
continue
_service = services.get(_srv)
if _service:
_service.construct_extra_headers.append(dpop_header)


class DPoPClientAuth(BearerHeader):
tag = "dpop_client_auth"

def construct(self, request=None, service=None, http_args=None, **kwargs):
"""
Constructing the Authorization header. The value of
the Authorization header is "Bearer <access_token>".

:param request: Request class instance
:param service: The service this authentication method applies to.
:param http_args: HTTP header arguments
:param kwargs: extra keyword arguments
:return:
"""

_token_type = "access_token"

_token_info = find_token_info(request, _token_type, service, **kwargs)

if not _token_info:
raise KeyError("No bearer token available")

# The authorization value starts with the token_type
# if _token_info["token_type"].to_lower() != "bearer":
_bearer = f"DPoP {_token_info[_token_type]}"

# Add 'Authorization' to the headers
if http_args is None:
http_args = {"headers": {}}
http_args["headers"]["Authorization"] = _bearer
else:
try:
http_args["headers"]["Authorization"] = _bearer
except KeyError:
http_args["headers"] = {"Authorization": _bearer}

return http_args
6 changes: 1 addition & 5 deletions src/idpyoidc/client/oauth2/add_on/par.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,9 @@ def push_authorization(request_args, service, **kwargs):
_context.client_authn_methods[_name] = execute(spec)
authn_method = _name

_args = {}
_args = kwargs.copy()
if _context.issuer:
_args["iss"] = _context.issuer
if _name == "client_attestation":
_wia = kwargs.get("client_attestation")
if _wia:
_args["client_attestation"] = _wia

_headers = service.get_headers(
request_args, http_method=_http_method, authn_method=authn_method, **_args
Expand Down
26 changes: 15 additions & 11 deletions src/idpyoidc/client/oauth2/server_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_endpoint(self):
:return: Service endpoint
"""
try:
_iss = self.upstream_get("context").issuer
_iss = self.upstream_get("attribute","issuer")
except AttributeError:
_iss = self.endpoint

Expand Down Expand Up @@ -72,13 +72,16 @@ def _verify_issuer(self, resp, issuer):

# In some cases we can live with the two URLs not being
# the same. But this is an excepted that has to be explicit
try:
self.upstream_get("context").allow["issuer_mismatch"]
except KeyError:
if _issuer != _pcr_issuer:
raise OidcServiceError(
"provider info issuer mismatch '%s' != '%s'" % (_issuer, _pcr_issuer)
)
_allow = self.upstream_get("attribute", "allow")
if _allow:
_allowed = _allow.get("issuer_mismatch", None)
if _allowed:
return _issuer

if _issuer != _pcr_issuer:
raise OidcServiceError(
"provider info issuer mismatch '%s' != '%s'" % (_issuer, _pcr_issuer)
)
return _issuer

def _set_endpoints(self, resp):
Expand Down Expand Up @@ -131,9 +134,10 @@ def _update_service_context(self, resp):
# is loaded not necessarily that any keys are fetched.
if "jwks_uri" in resp:
LOGGER.debug(f"'jwks_uri' in provider info: {resp['jwks_uri']}")
_hp = self.upstream_get('entity').httpc_params
if "verify" in _hp and "verify" not in _keyjar.httpc_params:
_keyjar.httpc_params["verify"] = _hp["verify"]
_hp = self.upstream_get("attribute","httpc_params")
if _hp:
if "verify" in _hp and "verify" not in _keyjar.httpc_params:
_keyjar.httpc_params["verify"] = _hp["verify"]
_keyjar.load_keys(_pcr_issuer, jwks_uri=resp["jwks_uri"])
_loaded = True
elif "jwks" in resp:
Expand Down
1 change: 0 additions & 1 deletion src/idpyoidc/client/oidc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def __init__(
jwks_uri: Optional[str] = "",
**kwargs
):
self.upstream_get = upstream_get
if services:
_srvs = services
else:
Expand Down
2 changes: 1 addition & 1 deletion src/idpyoidc/client/oidc/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def gather_verify_arguments(
:return: dictionary with arguments to the verify call
"""
_context = self.upstream_get("context")
_entity = self.upstream_get("entity")
_entity = self.upstream_get("unit")

kwargs = {
"client_id": _entity.get_client_id(),
Expand Down
2 changes: 1 addition & 1 deletion src/idpyoidc/client/oidc/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def update_service_context(self, resp, key="", **kwargs):
_context.cstate.update(key, resp)

def get_request_from_response(self, response):
_context = self.upstream_get("service_context")
_context = self.upstream_get("context")
return _context.cstate.get_set(response["state"], message=oauth2.AuthorizationRequest)

def post_parse_response(self, response, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion src/idpyoidc/client/oidc/provider_info_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def add_redirect_uris(request_args, service=None, **kwargs):
:param kwargs: Possible extra keyword arguments
:return: A possibly augmented set of request arguments.
"""
_work_environment = service.upstream_get("context").claims
_work_environment = service.upstream_get("attribute", "claims")
if "redirect_uris" not in request_args:
# Callbacks is a dictionary with callback type 'code', 'implicit',
# 'form_post' as keys.
Expand Down
9 changes: 8 additions & 1 deletion src/idpyoidc/client/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,16 @@ def construct(self, request_args: Optional[dict] = None, **kwargs):
_args = self.gather_request_args(**request_args)

# logger.debug("kwargs: %s" % sanitize(kwargs))

# we must check if claims module is idpyoidc.client.claims.oauth2recource as
# in that case we don't want to set_defaults like application_type etc.
obj = self.upstream_get("context").claims
# initiate the request as in an instance of the self.msg_type
# message type
request = self.msg_type(**_args)
if(obj.__class__.__module__ == "idpyoidc.client.claims.oauth2resource"):
request = self.msg_type(**_args, set_defaults=False)
else:
request = self.msg_type(**_args)

_behaviour_args = kwargs.get("behaviour_args")
if _behaviour_args:
Expand Down
7 changes: 7 additions & 0 deletions src/idpyoidc/client/service_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from idpyoidc.claims import claims_dump
from idpyoidc.claims import claims_load
from idpyoidc.client.claims.oauth2 import Claims as OAUTH2_Specs
from idpyoidc.client.claims.oauth2resource import Claims as OAUTH2RESOURCE_Specs
from idpyoidc.client.claims.oidc import Claims as OIDC_Specs
from idpyoidc.client.configure import Configuration
from idpyoidc.util import rndstr
Expand Down Expand Up @@ -133,6 +134,8 @@ def __init__(
self.claims = OIDC_Specs()
elif client_type == "oauth2":
self.claims = OAUTH2_Specs()
elif client_type == "oauth2resource":
self.claims = OAUTH2RESOURCE_Specs()
else:
raise ValueError(f"Unknown client type: {client_type}")

Expand All @@ -156,6 +159,7 @@ def __init__(
self.httpc_params = {}
self.client_secret_expires_at = 0
self.registration_response = {}
self.client_authn_methods = {}

# _def_value = copy.deepcopy(DEFAULT_VALUE)

Expand Down Expand Up @@ -186,6 +190,9 @@ def __init__(

self.construct_uris(response_types=_response_types)

self.map_supported_to_preferred()
self.map_preferred_to_registered()

def __setitem__(self, key, value):
setattr(self, key, value)

Expand Down
16 changes: 16 additions & 0 deletions src/idpyoidc/message/oauth2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,22 @@ class TokenRevocationErrorResponse(ResponseMessage):
c_allowed_values = ResponseMessage.c_allowed_values.copy()
c_allowed_values.update({"error": ["unsupported_token_type"]})

class OAuthProtectedResourceRequest(Message):
c_param = {
"resource": SINGLE_REQUIRED_STRING,
"authorization_servers": OPTIONAL_LIST_OF_STRINGS,
"jwks_uri": SINGLE_OPTIONAL_STRING,
"resource_documentation": SINGLE_OPTIONAL_STRING,
"scopes_supported": OPTIONAL_LIST_OF_STRINGS,
"bearer_methods_supported": OPTIONAL_LIST_OF_STRINGS,
"resource_signing_alg_values_supported": OPTIONAL_LIST_OF_STRINGS,
"resource_encryption_alg_values_supported": OPTIONAL_LIST_OF_STRINGS,
"resource_encryption_enc_values_supported": OPTIONAL_LIST_OF_STRINGS,
"client_registration_types": OPTIONAL_LIST_OF_STRINGS,
"organization_name": SINGLE_OPTIONAL_STRING,
"resource_policy_uri": SINGLE_OPTIONAL_STRING,
"resource_tos_uri": SINGLE_OPTIONAL_STRING
}

def factory(msgtype, **kwargs):
"""
Expand Down
6 changes: 3 additions & 3 deletions src/idpyoidc/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ def get_unit(self, *args):
def topmost_unit(unit):
if hasattr(unit, "upstream_get"):
if unit.upstream_get:
next_unit = unit.upstream_get("unit")
if next_unit:
unit = topmost_unit(next_unit)
superior = unit.upstream_get("unit")
if superior:
unit = topmost_unit(superior)

return unit

Expand Down
Loading
Loading