Skip to content

Commit

Permalink
Merge pull request #87 from IdentityPython/id_assurance
Browse files Browse the repository at this point in the history
Id assurance
  • Loading branch information
rohe authored Dec 17, 2023
2 parents 7364f05 + 1be342b commit 3690e6b
Show file tree
Hide file tree
Showing 57 changed files with 587 additions and 231 deletions.
7 changes: 1 addition & 6 deletions example/flask_op/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,8 @@
"request_uri_parameter_supported": true,
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"id_token token",
"code id_token token",
"none"
"code id_token"
],
"response_modes_supported": [
"query",
Expand Down
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": "gVdGvOn9TFG38gzqs2uO-pQB40qxJbfm"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "HlW_dFC6aquKPZ6zGtq2dZEBcntP9uHd"}]}
{"keys": [{"kty": "oct", "use": "enc", "kid": "enc", "k": "GCizp3ewVRV0VZEef3VQwFve7n2QwAFI"}, {"kty": "oct", "use": "sig", "kid": "sig", "k": "QC2JxpVJXPDMpYv_h76jIrt_lA1P4KSu"}]}
2 changes: 1 addition & 1 deletion example/flask_rp/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def init_oidc_rp_handler(app):
if _rp_conf.key_conf:
_kj = init_key_jar(**_rp_conf.key_conf)
_path = _rp_conf.key_conf['public_path']
# removes ./ and / from the begin of the string
# removes ./ and / from the begining of the string
_path = re.sub('^(.)/', '', _path)
else:
_kj = KeyJar()
Expand Down
38 changes: 19 additions & 19 deletions example/flask_rp/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,26 @@
},
"clients": {
"": {
"client_preferences": {
"httpc_params": {
"verify": false
},
"client_type": "oidc",
"capabilities": {
"application_name": "rphandler",
"metadata": {
"application_type": "web",
"contacts": [
"ops@example.com"
],
"response_types": [
"code"
]
},
"usage": {
"scope": [
"openid",
"profile",
"email",
"address",
"phone"
]
},
"application_type": "web",
"contacts": [
"ops@example.com"
],
"response_types_supported": [
"code"
],
"scopes_supported": [
"openid",
"profile",
"email",
"address",
"phone"
],
"token_endpoint_auth_methods": [
"client_secret_basic",
"client_secret_post"
Expand Down
2 changes: 1 addition & 1 deletion example/flask_rp/templates/opbyuid.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ <h3>By entering your unique identifier:</h3>
<input type="text" id="uid" name="uid" class="form-control" placeholder="UID" autofocus>
<h3>an issuer ID</h3>
<input type="text" id="dyn_iss" name="dyn_iss" class="form-control">
<h3><em>Or</em> you can chose one of the preconfigured OpenID Connect Providers</h3>
<h3><em>Or</em> you can choose one of the preconfigured OpenID Connect Providers</h3>
<select name="static_iss">
<option value=""></option>
{% for op in providers %}
Expand Down
13 changes: 4 additions & 9 deletions example/flask_rp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def rp():
except Exception as err:
return make_response('Something went wrong:{}'.format(err), 400)
else:
response = redirect(result['url'], 303)
response = redirect(result, 303)
return response
else:
_providers = current_app.rp_config.clients.keys()
Expand Down Expand Up @@ -106,7 +106,7 @@ def finalize(op_identifier, request_args):
session['state'] = request_args.get('state')

if session['state']:
iss = _context.state.get_iss(session['state'])
iss = _context.cstate.get_set(session['state'], claim=["iss"])['iss']
else:
return make_response('Unknown state', 400)

Expand Down Expand Up @@ -158,14 +158,9 @@ def get_op_identifier_by_cb_uri(url: str):
uri = splitquery(url)[0]
for k, v in current_app.rph.issuer2rp.items():
for endpoint in v.get_callback_uris():
_endps = v.get_metadata_value(endpoint)
if _endps is None:
continue
elif isinstance(_endps,str):
if _endps == uri:
for val in v.context.claims.get_preference(endpoint):
if val == uri:
return k
elif uri in _endps:
return k
return None

@oidc_rp_views.route('/authz_cb/<op_identifier>')
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ build-backend = "setuptools.build_meta"

[metadata]
name = "idpyoidc"
version = "2.1.0"
version = "3.0.0"
author = "Roland Hedberg"
author_email = "roland@catalogix.se"
description = "Everything OAuth2 and OIDC"
long_description = "file: README.md"
long_description_content_type = "text/markdown"
url = "https://github.com/IdentityPython/oidc-op"
url = "https://github.com/IdentityPython/idpy-oidc"
license = "Apache-2.0"
classifiers =[
"Programming Language :: Python :: 3",
Expand All @@ -31,7 +31,7 @@ line-length = 100

[tool.isort]
force_single_line = true
known_first_party = "oidcop"
known_first_party = "idpyoidc"
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def run_tests(self):
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules"],
install_requires=[
"cryptojwt>=1.8.1",
"cryptojwt>=1.8.3",
"pyOpenSSL",
"filelock>=3.0.12",
'pyyaml>=5.1.2',
Expand Down
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__ = "2.2.0"
__version__ = "3.0.0"

VERIFIED_CLAIM_PREFIX = "__verified"

Expand Down
2 changes: 1 addition & 1 deletion src/idpyoidc/claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def add_extra_keys(self, keyjar, id):
return None

def get_jwks(self, keyjar):
return None
return keyjar.export_jwks()

def handle_keys(self, configuration: dict, keyjar: Optional[KeyJar] = None):
_jwks = _jwks_uri = None
Expand Down
69 changes: 54 additions & 15 deletions src/idpyoidc/client/client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@

from idpyoidc.defaults import DEF_SIGN_ALG
from idpyoidc.defaults import JWT_BEARER
from idpyoidc.message.oauth2 import SINGLE_OPTIONAL_STRING
from idpyoidc.message import Message
from idpyoidc.message.oauth2 import AccessTokenRequest
from idpyoidc.message.oauth2 import SINGLE_OPTIONAL_STRING
from idpyoidc.message.oidc import AuthnToken
from idpyoidc.time_util import utc_time_sans_frac
from idpyoidc.util import rndstr

from .util import sanitize
from ..message import VREQUIRED
from ..util import instantiate
from .util import sanitize

# from idpyoidc.oidc.backchannel_authentication import ClientNotificationAuthn

Expand All @@ -29,6 +29,7 @@

__author__ = "roland hedberg"

DEFAULT_ACCESS_TOKEN_TYPE = "Bearer"

class AuthnFailure(Exception):
"""Unspecified Authentication failure"""
Expand Down Expand Up @@ -137,8 +138,8 @@ def _with_or_without_client_id(request, service):
:param service: A :py:class:`idpyoidc.client.service.Service` instance
"""
if (
isinstance(request, AccessTokenRequest)
and request["grant_type"] == "authorization_code"
isinstance(request, AccessTokenRequest)
and request["grant_type"] == "authorization_code"
):
if "client_id" not in request:
try:
Expand Down Expand Up @@ -228,7 +229,7 @@ def modify_request(self, request, service, **kwargs):
if not request["client_secret"]:
raise AuthnFailure("Missing client secret")

# Set the client_id in the the request
# Set the client_id in the request
request["client_id"] = _context.get_client_id()

def construct(self, request, service=None, http_args=None, **kwargs):
Expand Down Expand Up @@ -276,10 +277,42 @@ def find_token(request, token_type, service, **kwargs):
except KeyError:
# Get the latest acquired token.
_state = kwargs.get("state", kwargs.get("key"))
_arg = service.upstream_get("context").cstate.get_set(_state, claim=[token_type])
_arg = service.upstream_get("context").cstate.get_set(_state, claim=[token_type,
"token_type"])
return _arg.get("access_token")


def find_token_info(request: Union[Message, dict], token_type: str, service, **kwargs) -> dict:
"""
Token acquired by a previous run service.
:param token_type:
:param kwargs:
:return:
"""

if request is not None:
_token = request.get(token_type, None)
if _token:
del request[token_type]
# Required under certain circumstances :-) not under other
request.c_param[token_type] = SINGLE_OPTIONAL_STRING
return {token_type: _token, "token_type": DEFAULT_ACCESS_TOKEN_TYPE}

_state = kwargs.get("state", kwargs.get("key"))
if _state:
_token_info = service.upstream_get("context").cstate.get_set(
_state, claim=[token_type, "token_type"])
else:
_token_info = {"token_type": DEFAULT_ACCESS_TOKEN_TYPE}

_token = kwargs.get("access_token", None)
if _token:
return {token_type: _token, "token_type": _token_info["token_type"]}
else:
return _token_info


class BearerHeader(ClientAuthnMethod):
"""The bearer header authentication method."""

Expand All @@ -296,18 +329,20 @@ def construct(self, request=None, service=None, http_args=None, **kwargs):
"""

if service.service_name == "refresh_token":
_acc_token = find_token(request, "refresh_token", service, **kwargs)
_token_type = "refresh_token"
elif service.service_name == "token_exchange":
_acc_token = find_token(request, "subject_token", service, **kwargs)
_token_type = "subject_token"
else:
_acc_token = find_token(request, "access_token", service, **kwargs)
_token_type = "access_token"

if not _acc_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 'Bearer' when bearer tokens
# are used
_bearer = "Bearer {}".format(_acc_token)
# The authorization value starts with the token_type
# if _token_info["token_type"].to_lower() != "bearer":
_bearer = f"{_token_info['token_type']} {_token_info[_token_type]}"

# Add 'Authorization' to the headers
if http_args is None:
Expand Down Expand Up @@ -502,9 +537,11 @@ def _construct_client_assertion(self, service, **kwargs):
except KeyError:
_args = {}

_client_id = kwargs.get("client_id", _entity.client_id)

# construct the signed JWT with the assertions and add
# it as value to the 'client_assertion' claim of the request
return assertion_jwt(_entity.client_id, signing_key, audience, algorithm, **_args)
return assertion_jwt(_client_id, signing_key, audience, algorithm, **_args)

def modify_request(self, request, service, **kwargs):
"""
Expand Down Expand Up @@ -643,6 +680,8 @@ def single_authn_setup(name, spec):
else:
if spec is None:
cls = get_client_authn_class(name)
if cls is None:
cls = importer(name)
elif isinstance(spec, str):
cls = importer(spec)
else:
Expand Down
3 changes: 3 additions & 0 deletions src/idpyoidc/client/current.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ def create_state(self, **kwargs):
_key = self.create_key()
self._db[_key] = kwargs
return _key

def keys(self):
return self._db.keys()
2 changes: 1 addition & 1 deletion src/idpyoidc/client/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,4 @@ def import_keys(self, keyspec):
return _keyjar

def get_callback_uris(self):
return self.context.claims.callback_uri
return self.context.claims.get_preference("callback_uris")
19 changes: 11 additions & 8 deletions src/idpyoidc/client/oauth2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ def get_response(
:param kwargs:
:return:
"""
_data = kwargs.get("data")
if _data and not body:
body = _data

try:
resp = self.httpc(method, url, data=body, headers=headers, **self.httpc_params)
except Exception as err:
Expand All @@ -168,6 +172,8 @@ def get_response(

if 300 <= resp.status_code < 400:
return {"http_response": resp}
elif resp.status_code >= 400:
logger.error(f"HTTP error: {resp}")

if resp.status_code < 300:
if "keyjar" not in kwargs:
Expand Down Expand Up @@ -213,14 +219,10 @@ def service_request(

logger.debug(REQUEST_INFO.format(url, method, body, headers))

try:
response = service.get_response_ext(
url, method, body, response_body_type, headers, **kwargs
)
except AttributeError:
response = self.get_response(
service, url, method, body, response_body_type, headers, **kwargs
)
_get_response_func = getattr(self, "get_response_ext", getattr(self, "get_response"))
response = _get_response_func(
service, url, method, body, response_body_type, headers, **kwargs
)

if "error" in response:
pass
Expand Down Expand Up @@ -333,6 +335,7 @@ def dynamic_provider_info_discovery(client: Client, behaviour_args: Optional[dic
except KeyError:
pass

logger.debug(f"{service}")
response = client.do_request(service, behaviour_args=behaviour_args)
if is_error_message(response):
raise OidcServiceError(response["error"])
4 changes: 2 additions & 2 deletions src/idpyoidc/client/oauth2/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class AccessToken(Service):
"token_endpoint_auth_signing_alg": get_signing_algs,
}

def __init__(self, upstream_get, conf=None):
Service.__init__(self, upstream_get, conf=conf)
def __init__(self, upstream_get, conf=None, **kwargs):
Service.__init__(self, upstream_get, conf=conf, **kwargs)
self.pre_construct.append(self.oauth_pre_construct)

def update_service_context(self, resp, key: Optional[str] = "", **kwargs):
Expand Down
Loading

0 comments on commit 3690e6b

Please sign in to comment.