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

Response type handling #104

Merged
merged 15 commits into from
Aug 25, 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
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# .coveragerc to control coverage.py
[run]
branch = true
omit = */tests/*, */wsgi.py, fabfile.py, /usr/local/*, ./setup.py
source = .

[report]
show_missing = true
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
fail-fast: false
matrix:
python-version:
- '3.7'
- '3.8'
- '3.9'
- '3.10'
- '3.11'

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion doc/client/add_on/dpop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ in a client configuration.

'add_ons': {
"dpop": {
"function": "oidcrp.oauth2.add_on.dpop.add_support",
"function": "idpyoidc.client.oauth2.add_on.dpop.add_support",
"kwargs": {
"signing_algorithms": ["ES256", "ES512"]
}
Expand Down
35 changes: 32 additions & 3 deletions doc/client/add_on/pushed_authorization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,39 @@ Pushed Authorization
Introduction
------------

https://tools.ietf.org/id/draft-lodderstedt-oauth-par-00.html
https://datatracker.ietf.org/doc/html/rfc9126

The Internet draft defines the pushed authorization request endpoint,
The Internet draft defines the pushed authorization request (PAR) endpoint,
which allows clients to push the payload of an OAuth 2.0 authorization
request to the authorization server via a direct request and provides
them with a request URI that is used as reference to the data in a
subsequent authorization request.
subsequent authorization request.

-------------
Configuration
-------------

There is basically one things you can configure:

- authn_method
Which client authentication method that should be used at the pushed authorization endpoint.
Default is none.

-------
Example
-------

What you have to do is to add a *par* section to an *add_ons* section
in a client configuration.

.. code:: python

'add_ons': {
"par": {
"function": "idpyoidc.client.oauth2.add_on.par.add_support",
"kwargs": {
"authn_method": "private_key_jwt"
}
}
}

8 changes: 4 additions & 4 deletions example/flask_op/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
}
}
},
"capabilities": {
"preference": {
"subject_types_supported": [
"public",
"pairwise"
Expand Down Expand Up @@ -260,6 +260,7 @@
"verify": false
},
"issuer": "https://{domain}:{port}",
"entity_id": "https://{domain}:{port}",
"keys": {
"private_path": "private/jwks.json",
"key_defs": [
Expand All @@ -277,9 +278,8 @@
]
}
],
"public_path": "static/jwks.json",
"read_only": false,
"uri_path": "static/jwks.json"
"uri_path": "jwks"
},
"login_hint2acrs": {
"class": "idpyoidc.server.login_hint.LoginHint2Acrs",
Expand Down Expand Up @@ -349,6 +349,6 @@
"verify_user": false,
"port": 5000,
"domain": "127.0.0.1",
"debug": true
"debug": false
}
}
37 changes: 22 additions & 15 deletions example/flask_op/views.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import json
import os
import sys
import traceback
from typing import Union
from urllib.parse import urlparse

import werkzeug
from cryptojwt import as_unicode
from flask import Blueprint
from flask import Response
from flask import current_app
from flask import redirect
from flask import render_template
from flask import request
from flask import Response
from flask.helpers import make_response
from flask.helpers import send_from_directory

from idpyoidc.message.oauth2 import ResponseMessage
from idpyoidc.message.oidc import AccessTokenRequest
from idpyoidc.message.oidc import AuthorizationRequest
import werkzeug

from idpyoidc.server.exception import FailedAuthentication
from idpyoidc.server.exception import ClientAuthenticationError
from idpyoidc.server.exception import FailedAuthentication
from idpyoidc.server.oidc.token import Token

# logger = logging.getLogger(__name__)
Expand All @@ -29,8 +27,8 @@


def _add_cookie(resp: Response, cookie_spec: Union[dict, list]):
kwargs = {k:v
for k,v in cookie_spec.items()
kwargs = {k: v
for k, v in cookie_spec.items()
if k not in ('name',)}
kwargs["path"] = "/"
kwargs["samesite"] = "Lax"
Expand All @@ -44,15 +42,22 @@ def add_cookie(resp: Response, cookie_spec: Union[dict, list]):
elif isinstance(cookie_spec, dict):
_add_cookie(resp, cookie_spec)

@oidc_op_views.route('/static/<path:path>')
def send_js(path):
return send_from_directory('static', path)

# @oidc_op_views.route('/static/<path:path>')
# def send_js(path):
# return send_from_directory('static', path)
#
#
# @oidc_op_views.route('/keys/<jwks>')
# def keys(jwks):
# fname = os.path.join('static', jwks)
# return open(fname).read()
#

@oidc_op_views.route('/keys/<jwks>')
def keys(jwks):
fname = os.path.join('static', jwks)
return open(fname).read()
@oidc_op_views.route('/jwks')
def jwks():
_context = current_app.server.get_context()
return _context.keyjar.export_jwks()


@oidc_op_views.route('/')
Expand Down Expand Up @@ -188,11 +193,13 @@ def token():
return service_endpoint(
current_app.server.get_endpoint('token'))


@oidc_op_views.route('/introspection', methods=['POST'])
def introspection_endpoint():
return service_endpoint(
current_app.server.get_endpoint('introspection'))


@oidc_op_views.route('/userinfo', methods=['GET', 'POST'])
def userinfo():
return service_endpoint(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers =[
[options]
package_dir = "src"
packages = "find:"
python= "^3.6"
python= "^3.8"

[tool.black]
line-length = 100
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,13 @@ def run_tests(self):
classifiers=[
"Development Status :: 4 - Beta",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Libraries :: Python Modules"],
install_requires=[
"cryptojwt>=1.8.3",
"cryptojwt>=1.8.4",
"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__ = "4.2.0"
__version__ = "4.3.0"

VERIFIED_CLAIM_PREFIX = "__verified"

Expand Down
9 changes: 7 additions & 2 deletions src/idpyoidc/client/oauth2/add_on/dpop.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def dpop_header(
headers: Optional[dict] = None,
token: Optional[str] = "",
nonce: Optional[str] = "",
endpoint_url: Optional[str] = "",
**kwargs
) -> dict:
"""
Expand All @@ -114,7 +115,11 @@ def dpop_header(
:return:
"""

provider_info = service_context.provider_info
if not endpoint_url:
endpoint_url = kwargs.get("endpoint")
if not endpoint_url:
endpoint_url = service_context.provider_info[service_endpoint]

_dpop_conf = service_context.add_on.get("dpop")
if not _dpop_conf:
logger.warning("Asked to do dpop when I do not support it")
Expand All @@ -139,7 +144,7 @@ def dpop_header(
"jwk": dpop_key.serialize(),
"jti": uuid.uuid4().hex,
"htm": http_method,
"htu": provider_info[service_endpoint],
"htu": endpoint_url,
"iat": utc_time_sans_frac(),
}

Expand Down
66 changes: 21 additions & 45 deletions src/idpyoidc/client/oauth2/add_on/par.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import logging

from cryptojwt import JWT
from cryptojwt.utils import importer

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__)

HTTP_METHOD = "POST"


def push_authorization(request_args, service, **kwargs):
"""
Expand All @@ -25,12 +25,6 @@ def push_authorization(request_args, service, **kwargs):
logger.debug(f"PAR method args: {method_args}")
logger.debug(f"PAR kwargs: {kwargs}")

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

_http_method = method_args["http_client"]
_httpc_params = service.upstream_get("unit").httpc_params

# Add client authentication if needed
_headers = {}
authn_method = method_args["authn_method"]
Expand All @@ -50,29 +44,22 @@ def push_authorization(request_args, service, **kwargs):
_args["iss"] = _context.issuer

_headers = service.get_headers(
request_args, http_method=_http_method, authn_method=authn_method, **_args
request_args, http_method=HTTP_METHOD, authn_method=authn_method, **_args
)
_headers["Content-Type"] = "application/x-www-form-urlencoded"

# construct the message body
if method_args["body_format"] == "urlencoded":
_body = request_args.to_urlencoded()
else:
_jwt = JWT(
key_jar=service.upstream_get("attribute", "keyjar"),
iss=_context.claims.prefer["client_id"],
)
_jws = _jwt.pack(request_args.to_dict())
_body = request_args.to_urlencoded()

_msg = Message(request=_jws)
for param in request_args.required_parameters():
_msg[param] = request_args.get(param)
_http_client = method_args.get("http_client", None)
if not _http_client:
_http_client = service.upstream_get("unit").httpc

_body = _msg.to_urlencoded()
_httpc_params = service.upstream_get("unit").httpc_params

# Send it to the Pushed Authorization Request Endpoint using POST
resp = _http_method(
method="POST",
resp = _http_client(
method=HTTP_METHOD,
url=_context.provider_info["pushed_authorization_request_endpoint"],
data=_body,
headers=_headers,
Expand All @@ -95,41 +82,30 @@ 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,
http_client=None,
authn_method="",
):
"""
Add the necessary pieces to support Pushed authorization.

:param merge_rule:
:param http_client:
:param signing_algorithm:
:param http_client: Specification for a HTTP client to use different from the default
:param authn_method: The client authentication method to use
:param services: A dictionary with all the services the client has access to.
:param body_format: jws or urlencoded
"""

if http_client is None:
_http_client = request
else:
if http_client is not None:
if isinstance(http_client, dict):
if "class" in http_client:
_http_client = instantiate(http_client["class"], **http_client.get("kwargs", {}))
http_client = instantiate(http_client["class"], **http_client.get("kwargs", {}))
else:
_http_client = importer(http_client["function"])
http_client = importer(http_client["function"])
else:
_http_client = importer(http_client)
http_client = importer(http_client)

_service = services["authorization"]
_service = services["authorization"] # There must be such a service
_service.upstream_get("context").add_on["pushed_authorization"] = {
"body_format": body_format,
"signing_algorithm": signing_algorithm,
"http_client": _http_client,
"merge_rule": merge_rule,
"apply": True,
"http_client": http_client,
"authn_method": authn_method,
}

Expand Down
6 changes: 5 additions & 1 deletion src/idpyoidc/client/oidc/access_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ def gather_verify_arguments(
_context = self.upstream_get("context")
_entity = self.upstream_get("unit")

_client_id = _entity.get_client_id()
if not _client_id:
_client_id = _context.get_client_id()

kwargs = {
"client_id": _entity.get_client_id(),
"client_id": _client_id,
"iss": _context.issuer,
"keyjar": self.upstream_get("attribute", "keyjar"),
"verify": True,
Expand Down
Loading
Loading