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

Openid4v #93

Closed
wants to merge 13 commits into from
Closed
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 example/flask_op/private/cookie_jwks.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "GCizp3ewVRV0VZEef3VQwFve7n2QwAFI"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "QC2JxpVJXPDMpYv_h76jIrt_lA1P4KSu"}]}
{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "kB4Z6SmpYe0URDyigVNHi25PeZ1MPG_B"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "1HMtbIxBQISnRkLSnyf_wJOJ0SzYp6pC"}]}
57 changes: 52 additions & 5 deletions src/idpyoidc/client/oauth2/add_on/dpop.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from cryptojwt.jws.jws import factory
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 SINGLE_OPTIONAL_STRING
from idpyoidc.message import SINGLE_REQUIRED_INT
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 @@ -181,7 +183,52 @@ def add_support(services, dpop_signing_alg_values_supported):

_service.construct_extra_headers.append(dpop_header)

# The same for userinfo requests
_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
54 changes: 37 additions & 17 deletions src/idpyoidc/client/oauth2/add_on/par.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import logging
from typing import Union

from cryptojwt import JWT
from cryptojwt.utils import importer
from requests import request

from idpyoidc.client.client_auth import CLIENT_AUTHN_METHOD
from idpyoidc.message import Message
from idpyoidc.message.oauth2 import JWTSecuredAuthorizationRequest
from idpyoidc.server.util import execute
from idpyoidc.util import instantiate
from requests import request

logger = logging.getLogger(__name__)


def push_authorization(request_args, service, **kwargs):
def get_request_parameters(request_args: Union[dict, Message], service, **kwargs) -> dict:
"""
:param request_args: All the request arguments as a AuthorizationRequest instance
:param service: The service to which this post construct method is applied.
Expand All @@ -26,20 +27,21 @@ def push_authorization(request_args, service, **kwargs):
logger.debug(f"PAR kwargs: {kwargs}")

if method_args["apply"] is False:
return request_args
return {"request_args": request_args}

_http_method = method_args["http_client"]
_httpc_params = service.upstream_get("unit").httpc_params
logger.debug(f"httpc_params: {_httpc_params}")

# Add client authentication if needed
_headers = {}
authn_method = method_args["authn_method"]
if authn_method:
_name = ""
if isinstance(authn_method, str):
if authn_method not in _context.client_authn_methods:
_context.client_authn_methods[authn_method] = CLIENT_AUTHN_METHOD[authn_method]()
else:
_name = ""
for _name, spec in authn_method.items():
if _name not in _context.client_authn_methods:
_context.client_authn_methods[_name] = execute(spec)
Expand All @@ -48,10 +50,10 @@ def push_authorization(request_args, service, **kwargs):
_args = {}
if _context.issuer:
_args["iss"] = _context.issuer
if _name == "client_attestation":
_wia = kwargs.get("client_attestation")
if _name == 'client_authentication_attestation':
_wia = kwargs.get('wallet_instance_attestation')
if _wia:
_args["client_attestation"] = _wia
_args["attestation"] = _wia

_headers = service.get_headers(
request_args, http_method=_http_method, authn_method=authn_method, **_args
Expand All @@ -60,6 +62,8 @@ def push_authorization(request_args, service, **kwargs):

# construct the message body
if method_args["body_format"] == "urlencoded":
if isinstance(request_args, dict):
request_args = Message(**request_args)
_body = request_args.to_urlencoded()
else:
_jwt = JWT(
Expand All @@ -74,13 +78,29 @@ def push_authorization(request_args, service, **kwargs):

_body = _msg.to_urlencoded()

return {
"http_method": _http_method,
"body": _body,
"headers": _headers,
"httpc_params": _httpc_params
}


def push_authorization(request_args, service, **kwargs):
_req_info = get_request_parameters(request_args=request_args, service=service, **kwargs)
if "request_args" in _req_info:
return _req_info["request_args"]

_context = service.upstream_get("context")

# Send it to the Pushed Authorization Request Endpoint using POST
resp = _http_method(
kwargs = _req_info.get("httpc_params", {})
resp = _req_info["http_method"](
method="POST",
url=_context.provider_info["pushed_authorization_request_endpoint"],
data=_body,
headers=_headers,
**_httpc_params
data=_req_info["body"],
headers=_req_info["headers"],
**kwargs
)

if resp.status_code == 200:
Expand All @@ -99,12 +119,12 @@ def push_authorization(request_args, service, **kwargs):


def add_support(
services,
body_format="jws",
signing_algorithm="RS256",
http_client=None,
merge_rule="strict",
authn_method="",
services,
body_format="jws",
signing_algorithm="RS256",
http_client=None,
merge_rule="strict",
authn_method="",
):
"""
Add the necessary pieces to support Pushed authorization.
Expand Down
4 changes: 2 additions & 2 deletions src/idpyoidc/client/oauth2/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class Authorization(Service):
response_body_type = "urlencoded"

_supports = {
"response_types_supported": ["code"],
"response_modes_supported": ["query", "fragment"],
"response_types": ["code"],
"response_modes": ["query", "fragment"],
}

_callback_path = {
Expand Down
29 changes: 12 additions & 17 deletions src/idpyoidc/client/oauth2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from idpyoidc.client.defaults import DEFAULT_RESPONSE_MODE
from idpyoidc.client.service import Service
from idpyoidc.exception import MissingParameter
from idpyoidc.exception import MissingRequiredAttribute
from idpyoidc.message import Message

Expand All @@ -13,22 +12,14 @@

def get_state_parameter(request_args, kwargs):
"""Find a state value from a set of possible places."""
try:
_state = kwargs["state"]
except KeyError:
try:
_state = request_args["state"]
except KeyError:
raise MissingParameter("state")

return _state
return kwargs.get("state", request_args.get("state", None))


def pick_redirect_uri(
context,
request_args: Optional[Union[Message, dict]] = None,
response_type: Optional[str] = "",
response_mode: Optional[str] = "",
context,
request_args: Optional[Union[Message, dict]] = None,
response_type: Optional[str] = "",
response_mode: Optional[str] = "",
):
if request_args is None:
request_args = {}
Expand Down Expand Up @@ -87,7 +78,7 @@ def pick_redirect_uri(


def pre_construct_pick_redirect_uri(
request_args: Optional[Union[Message, dict]] = None, service: Optional[Service] = None, **kwargs
request_args: Optional[Union[Message, dict]] = None, service: Optional[Service] = None, **kwargs
):
request_args["redirect_uri"] = pick_redirect_uri(
service.upstream_get("context"), request_args=request_args
Expand All @@ -97,5 +88,9 @@ def pre_construct_pick_redirect_uri(

def set_state_parameter(request_args=None, **kwargs):
"""Assigned a state value."""
request_args["state"] = get_state_parameter(request_args, kwargs)
return request_args, {"state": request_args["state"]}
_state = get_state_parameter(request_args, kwargs)
if _state:
request_args["state"] = _state
return request_args, {"state": request_args["state"]}
else:
return request_args, {}
4 changes: 3 additions & 1 deletion src/idpyoidc/client/oidc/userinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ def post_parse_response(self, response, **kwargs):
return response

def gather_verify_arguments(
self, response: Optional[Union[dict, Message]] = None, behaviour_args: Optional[dict] = None
self,
response: Optional[Union[dict, Message]] = None,
behaviour_args: Optional[dict] = None
):
"""
Need to add some information before running verify()
Expand Down
5 changes: 3 additions & 2 deletions src/idpyoidc/client/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def __init__(
self.client_authn_methods = {}

if conf:
LOGGER.debug(f"Service config: {conf}")
self.conf = conf
for param in [
"msg_type",
Expand All @@ -110,8 +111,8 @@ def __init__(
if _client_authn_methods:
self.client_authn_methods = client_auth_setup(method_to_item(_client_authn_methods))

if self.default_authn_method:
if self.default_authn_method not in self.client_authn_methods:
if not self.client_authn_methods:
if self.default_authn_method:
self.client_authn_methods[self.default_authn_method] = single_authn_setup(
self.default_authn_method, None
)
Expand Down
13 changes: 10 additions & 3 deletions src/idpyoidc/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,17 @@ def __init__(
add_base_path(conf, base_path, self._dir_attributes, "dir")

# entity info
self.domain = domain or conf.get("domain", "127.0.0.1")
self.port = port or conf.get("port", 80)
if domain != "":
self.domain = domain
else:
self.domain = conf.get("domain", "127.0.0.1")
if port != 0:
self.port = port
else:
self.port = conf.get("port", 80)

self.conf = set_domain_and_port(conf, self.domain, self.port)
if self.domain:
self.conf = set_domain_and_port(conf, self.domain, self.port)

def __getattr__(self, item, default=None):
if item in self:
Expand Down
2 changes: 2 additions & 0 deletions src/idpyoidc/impexp.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def dump_attr(self, cls, item, exclude_attributes: Optional[List[str]] = None) -
val = qualified_name(item)
elif isinstance(cls, list):
val = [self.dump_attr(cls[0], v, exclude_attributes) for v in item]
elif isinstance(item, dict):
val = {k: self.dump_attr(type2cls(v), v, exclude_attributes) for k, v in item.items()}
else:
val = item.dump(exclude_attributes=exclude_attributes)

Expand Down
13 changes: 11 additions & 2 deletions src/idpyoidc/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,16 @@ def __init__(
entity_id: Optional[str] = "",
key_conf: Optional[dict] = None,
):
self.entity_id = entity_id or conf.get("entity_id")
self.issuer = conf.get("issuer", self.entity_id)
# issuer == entity_id
# entity_id as parameter has higher precedence then in conf
_iss = entity_id or conf.get("entity_id", "")
if _iss:
self.entity_id = self.issuer = _iss
else:
_iss = conf.get("issuer", "")
if _iss:
self.entity_id = self.issuer = _iss

self.persistence = None

if upstream_get is None:
Expand Down Expand Up @@ -81,6 +89,7 @@ def __init__(
cwd=cwd,
cookie_handler=cookie_handler,
keyjar=self.keyjar,
entity_id=self.entity_id
)

# Need to have context in place before doing this
Expand Down
5 changes: 4 additions & 1 deletion src/idpyoidc/server/client_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def _verify(
res = super()._verify(
request=request, key_type="client_secret", endpoint=endpoint, **kwargs
)
# Verify that a HS alg was used
# Verify that an HS alg was used
return res


Expand Down Expand Up @@ -487,9 +487,12 @@ def verify_client(
if not allowed_methods:
allowed_methods = list(methods.keys()) # If not specific for this endpoint then all

logger.debug(f"Allowed client authentication methods: {allowed_methods}")
_method = None
_cinfo = {}
for _method in (methods[meth] for meth in allowed_methods):
if not _method.is_usable(request=request, authorization_token=authorization_token):
logger.debug(f"{_method} not usable")
continue
try:
logger.info(f"Verifying client authentication using {_method.tag}")
Expand Down
Loading
Loading