diff --git a/.gitignore b/.gitignore index 831dbd0f..d2508374 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ conf.yaml flask_op/debug.log flask_op/static/ debug.log +.pytest_cache/ # Created by .ignore support plugin (hsz.mobi) ### Python template # Byte-compiled / optimized / DLL files diff --git a/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png b/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png new file mode 100644 index 00000000..f226aa8c Binary files /dev/null and b/docs/source/_images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py index 34382356..2cc78e31 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) -from recommonmark.parser import CommonMarkParser +# from recommonmark.parser import CommonMarkParser # -- Project information ----------------------------------------------------- diff --git a/docs/source/contents/clients.rst b/docs/source/contents/clients.rst new file mode 100644 index 00000000..f5644c91 --- /dev/null +++ b/docs/source/contents/clients.rst @@ -0,0 +1,75 @@ +******************** +The clients database +******************** + +Information kept about clients in the client database are to begin with the +client metadata as defined in +https://openid.net/specs/openid-connect-registration-1_0.html . + +To that we have the following additions specified in OIDC extensions. + +* https://openid.net/specs/openid-connect-rpinitiated-1_0.html + + post_logout_redirect_uri +* https://openid.net/specs/openid-connect-frontchannel-1_0.html + + frontchannel_logout_uri + + frontchannel_logout_session_required +* https://openid.net/specs/openid-connect-backchannel-1_0.html#Backchannel + + backchannel_logout_uri + + backchannel_logout_session_required +* https://openid.net/specs/openid-connect-federation-1_0.html#rfc.section.3.1 + + client_registration_types + + organization_name + + signed_jwks_uri + +And finally we add a number of parameters that are OidcOP specific. +These are described in this document. + +-------------- +allowed_scopes +-------------- + +Which scopes that can be returned to a client. This is used to filter +the set of scopes a user can authorize release of. + +----------------- +token_usage_rules +----------------- + +There are usage rules for tokens. Rules are set per token type (the basic set is +authorization_code, refresh_token, access_token and id_token). +The possible rules are: + ++ how many times they can be used ++ if other tokens can be minted based on this token ++ how fast they expire + +A typical example (this is the default) would be:: + + "token_usage_rules": { + "authorization_code": { + "max_usage": 1 + "supports_minting": ["access_token", "refresh_token"], + "expires_in": 600, + }, + "refresh_token": { + "supports_minting": ["access_token"], + "expires_in": -1 + }, + } + +This then means that access_tokens can be used any number of times, +can not be used to mint other tokens and will expire after 300 seconds +which is the default for any token. An authorization_code can only used once +and it can be used to mint access_tokens and refresh_tokens. Note that normally +an authorization_code is used to mint an access_token and a refresh_token at +the same time. Such a dual minting is counted as one usage. +And lastly an refresh_token can be used to mint access_tokens any number of +times. An *expires_in* of -1 means that the token will never expire. + +If token_usage_rules are defined in the client metadata then it will be used +whenever a token is minted unless circumstances makes the OP modify the rules. + +Also this does not mean that what is valid for a token can not be changed +during run time. + + diff --git a/docs/source/contents/conf.rst b/docs/source/contents/conf.rst index e5450105..30ed142f 100644 --- a/docs/source/contents/conf.rst +++ b/docs/source/contents/conf.rst @@ -199,9 +199,9 @@ client_db If you're running an OP with static client registration you want to keep the registered clients in a database separate from the session database since -it will change independent of the OP process. In this case you need this. +it will change independent of the OP process. In this case you need *client_db*. If you are on the other hand only allowing dynamic client registration then -keeping registered clients in the session database makes total sense. +keeping registered clients only in the session database makes total sense. The class you reference in the specification MUST be a subclass of oidcmsg.storage.DictType and have some of the methods a dictionary has. diff --git a/docs/source/contents/session_management.rst b/docs/source/contents/session_management.rst index 26e99371..8bb60fbf 100644 --- a/docs/source/contents/session_management.rst +++ b/docs/source/contents/session_management.rst @@ -481,7 +481,7 @@ add_grant +++++++++ .. _add_grant: - add_grant(self, user_id, client_id, **kwargs) + add_grant(self, user_id, client_id, \*\*kwargs) find_token ++++++++++ diff --git a/docs/source/index.rst b/docs/source/index.rst index 99a12111..051a7b27 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,10 @@ Welcome to Idpy OIDC-op Documentation ====================================== +.. image:: _images/oid-l-certification-mark-l-rgb-150dpi-90mm-300x157.png + :width: 300 + :alt: OIDC Certified + This project is a Python implementation of an **OIDC Provider** on top of `jwtconnect.io `_ that shows you how to 'build' an OP using the classes and functions provided by oidc-op. @@ -66,6 +70,12 @@ under the `Apache 2.0 `_. contents/developers.md +.. toctree:: + :maxdepth: 2 + :caption: Client database + + contents/clients.rst + .. toctree:: :maxdepth: 2 :caption: FAQ diff --git a/example/fastapi/__init__.py b/example/fastapi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/example/fastapi/config.json b/example/fastapi/config.json new file mode 100644 index 00000000..0dc7fd0d --- /dev/null +++ b/example/fastapi/config.json @@ -0,0 +1,330 @@ +{ + "add_on": { + "pkce": { + "function": "oidcop.oidc.add_on.pkce.add_pkce_support", + "kwargs": { + "essential": false, + "code_challenge_method": "S256 S384 S512" + } + }, + "claims": { + "function": "oidcop.oidc.add_on.custom_scopes.add_custom_scopes", + "kwargs": { + "research_and_scholarship": [ + "name", + "given_name", + "family_name", + "email", + "email_verified", + "sub", + "iss", + "eduperson_scoped_affiliation" + ] + } + } + }, + "authz": { + "class": "oidcop.authz.AuthzHandling", + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "supports_minting": [ + "access_token", + "refresh_token", + "id_token" + ], + "max_usage": 1 + }, + "access_token": {}, + "refresh_token": { + "supports_minting": [ + "access_token", + "refresh_token" + ] + } + }, + "expires_in": 43200 + } + } + }, + "authentication": { + "user": { + "acr": "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD", + "class": "oidcop.user_authn.user.UserPassJinja2", + "kwargs": { + "verify_endpoint": "verify/user", + "template": "user_pass.jinja2", + "db": { + "class": "oidcop.util.JSONDictDB", + "kwargs": { + "filename": "passwd.json" + } + }, + "page_header": "Testing log in", + "submit_btn": "Get me in!", + "user_label": "Nickname", + "passwd_label": "Secret sauce" + } + } + }, + "capabilities": { + "subject_types_supported": [ + "public", + "pairwise" + ], + "grant_types_supported": [ + "authorization_code", + "implicit", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "refresh_token" + ] + }, + "cookie_handler": { + "class": "oidcop.cookie_handler.CookieHandler", + "kwargs": { + "keys": { + "private_path": "private/cookie_jwks.json", + "key_defs": [ + { + "type": "OCT", + "use": [ + "enc" + ], + "kid": "enc" + }, + { + "type": "OCT", + "use": [ + "sig" + ], + "kid": "sig" + } + ], + "read_only": false + }, + "name": { + "session": "oidc_op", + "register": "oidc_op_rp", + "session_management": "sman" + } + } + }, + "endpoint": { + "webfinger": { + "path": ".well-known/webfinger", + "class": "oidcop.oidc.discovery.Discovery", + "kwargs": { + "client_authn_method": null + } + }, + "provider_info": { + "path": ".well-known/openid-configuration", + "class": "oidcop.oidc.provider_config.ProviderConfiguration", + "kwargs": { + "client_authn_method": null + } + }, + "registration": { + "path": "registration", + "class": "oidcop.oidc.registration.Registration", + "kwargs": { + "client_authn_method": null, + "client_secret_expiration_time": 432000 + } + }, + "registration_api": { + "path": "registration_api", + "class": "oidcop.oidc.read_registration.RegistrationRead", + "kwargs": { + "client_authn_method": [ + "bearer_header" + ] + } + }, + "introspection": { + "path": "introspection", + "class": "oidcop.oauth2.introspection.Introspection", + "kwargs": { + "client_authn_method": [ + "client_secret_post" + ], + "release": [ + "username" + ] + } + }, + "authorization": { + "path": "authorization", + "class": "oidcop.oidc.authorization.Authorization", + "kwargs": { + "client_authn_method": null, + "claims_parameter_supported": true, + "request_parameter_supported": true, + "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" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ] + } + }, + "token": { + "path": "token", + "class": "oidcop.oidc.token.Token", + "kwargs": { + "client_authn_method": [ + "client_secret_post", + "client_secret_basic", + "client_secret_jwt", + "private_key_jwt" + ] + } + }, + "userinfo": { + "path": "userinfo", + "class": "oidcop.oidc.userinfo.UserInfo", + "kwargs": { + "claim_types_supported": [ + "normal", + "aggregated", + "distributed" + ] + } + }, + "end_session": { + "path": "session", + "class": "oidcop.oidc.session.Session", + "kwargs": { + "logout_verify_url": "verify_logout", + "post_logout_uri_path": "post_logout", + "signing_alg": "ES256", + "frontchannel_logout_supported": true, + "frontchannel_logout_session_supported": true, + "backchannel_logout_supported": true, + "backchannel_logout_session_supported": true, + "check_session_iframe": "check_session_iframe" + } + } + }, + "httpc_params": { + "verify": false + }, + "id_token": { + "class": "oidcop.id_token.IDToken", + "kwargs": { + "default_claims": { + "email": { + "essential": true + }, + "email_verified": { + "essential": true + } + } + } + }, + "issuer": "https://{domain}:{port}", + "keys": { + "private_path": "private/jwks.json", + "key_defs": [ + { + "type": "RSA", + "use": [ + "sig" + ] + }, + { + "type": "EC", + "crv": "P-256", + "use": [ + "sig" + ] + } + ], + "public_path": "static/jwks.json", + "read_only": false, + "uri_path": "static/jwks.json" + }, + "login_hint2acrs": { + "class": "oidcop.login_hint.LoginHint2Acrs", + "kwargs": { + "scheme_map": { + "email": [ + "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD" + ] + } + } + }, + "session_key": { + "filename": "private/session_jwk.json", + "type": "OCT", + "use": "sig" + }, + "template_dir": "templates", + "token_handler_args": { + "jwks_def": { + "private_path": "private/token_jwks.json", + "read_only": false, + "key_defs": [ + { + "type": "oct", + "bytes": 24, + "use": [ + "enc" + ], + "kid": "code" + }, + { + "type": "oct", + "bytes": 24, + "use": [ + "enc" + ], + "kid": "refresh" + } + ] + }, + "code": { + "kwargs": { + "lifetime": 600 + } + }, + "token": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "add_claims": [ + "email", + "email_verified", + "phone_number", + "phone_number_verified" + ], + "add_claim_by_scope": true, + "aud": [ + "https://example.org/appl" + ] + } + }, + "refresh": { + "kwargs": { + "lifetime": 86400 + } + } + }, + "userinfo": { + "class": "oidcop.user_info.UserInfo", + "kwargs": { + "db_file": "users.json" + } + } +} diff --git a/example/fastapi/main.py b/example/fastapi/main.py new file mode 100644 index 00000000..e7e64c4b --- /dev/null +++ b/example/fastapi/main.py @@ -0,0 +1,64 @@ +import json +import logging + +from fastapi import Depends +from fastapi import FastAPI +from fastapi import HTTPException +from fastapi.logger import logger +from fastapi.openapi.models import Response +from models import AuthorizationRequest +from models import WebFingerRequest +from utils import verify + +from oidcop.exception import FailedAuthentication +from oidcop.server import Server + +logger.setLevel(logging.DEBUG) + +app = FastAPI() +app.server = None + + +def get_app(): + return app + + +@app.on_event("startup") +def op_startup(): + _str = open('config.json').read() + cnf = json.loads(_str) + server = Server(cnf, cwd="/oidc") + app.server = server + + +@app.get("/.well-known/webfinger") +async def well_known(model: WebFingerRequest = Depends()): + endpoint = app.server.server_get("endpoint", "discovery") + args = endpoint.process_request(model.dict()) + response = endpoint.do_response(**args) + resp = json.loads(response["response"]) + return resp + + +@app.get("/.well-known/openid-configuration") +async def openid_config(): + endpoint = app.server.server_get("endpoint", "provider_config") + args = endpoint.process_request() + response = endpoint.do_response(**args) + resp = json.loads(response["response"]) + return resp + + +@app.post('/verify/user', status_code=200) +def verify_user(kwargs: dict, response: Response): + authn_method = app.server.server_get( + "endpoint_context").authn_broker.get_method_by_id('user') + try: + return verify(app, authn_method, kwargs, response) + except FailedAuthentication as exc: + raise HTTPException(404, "Failed authentication") + + +@app.get('/authorization') +def authorization(model: AuthorizationRequest = Depends()): + return service_endpoint(app.server.server_get("endpoint", 'authorization')) diff --git a/example/fastapi/models.py b/example/fastapi/models.py new file mode 100644 index 00000000..63b7a45e --- /dev/null +++ b/example/fastapi/models.py @@ -0,0 +1,31 @@ +from typing import List +from typing import Optional + +from pydantic import BaseModel + + +class WebFingerRequest(BaseModel): + rel: Optional[str] = 'http://openid.net/specs/connect/1.0/issuer' + resource: str + + +class AuthorizationRequest(BaseModel): + acr_values: Optional[List[str]] + claims: Optional[dict] + claims_locales: Optional[List[str]] + client_id: str + display: Optional[str] + id_token_hint: Optional[str] + login_hint: Optional[str] + max_age: Optional[int] + nonce: Optional[str] + prompt: Optional[List[str]] + redirect_uri: str + registration: Optional[dict] + request: Optional[str] + request_uri: Optional[str] + response_mode: Optional[str] + response_type: List[str] + scope: List[str] + state: Optional[str] + ui_locales: Optional[List[str]] diff --git a/unsupported/chpy/passwd.json b/example/fastapi/passwd.json similarity index 100% rename from unsupported/chpy/passwd.json rename to example/fastapi/passwd.json diff --git a/unsupported/chpy/users.json b/example/fastapi/users.json similarity index 100% rename from unsupported/chpy/users.json rename to example/fastapi/users.json diff --git a/example/fastapi/utils.py b/example/fastapi/utils.py new file mode 100644 index 00000000..13efdf6c --- /dev/null +++ b/example/fastapi/utils.py @@ -0,0 +1,136 @@ +import json + +from fastapi import HTTPException +from fastapi import status +from oic.oic import AuthorizationRequest +from oidcmsg.oauth2 import ResponseMessage + + +def do_response(endpoint, req_args, response, error='', **args): + info = endpoint.do_response(request=req_args, error=error, **args) + + try: + _response_placement = info['response_placement'] + except KeyError: + _response_placement = endpoint.response_placement + + if error: + if _response_placement == 'body': + raise HTTPException(400, info['response']) + else: # _response_placement == 'url': + response.status_code = status.HTTP_307_TEMPORARY_REDIRECT + resp = json.loads(info['response']) + else: + if _response_placement == 'body': + resp = json.loads(info['response']) + else: # _response_placement == 'url': + response.status_code = status.HTTP_307_TEMPORARY_REDIRECT + resp = json.loads(info['response']) + + for key, value in info['http_headers']: + response.headers[key] = value + + if 'cookie' in info: + for d in info["cookie"]: + response.set_cookie(key=d["name"], value=d["value"]) + + return resp + + +def verify(app, authn_method, kwargs, response): + """ + Authentication verification + + :param kwargs: response arguments + :return: HTTP redirect + """ + + #kwargs = dict([(k, v) for k, v in request.form.items()]) + username = authn_method.verify(**kwargs) + if not username: + raise HTTPException(403, "Authentication failed") + + auth_args = authn_method.unpack_token(kwargs['token']) + authz_request = AuthorizationRequest().from_urlencoded(auth_args['query']) + + endpoint = app.server.server_get("endpoint", 'authorization') + _session_id = endpoint.create_session(authz_request, username, auth_args['authn_class_ref'], + auth_args['iat'], authn_method) + + args = endpoint.authz_part2(request=authz_request, session_id=_session_id) + + if isinstance(args, ResponseMessage) and 'error' in args: + raise HTTPException(400, args.to_json()) + + return do_response(endpoint, kwargs, response, **args) + + +IGNORE = ["cookie", "user-agent"] + + +def service_endpoint(app, endpoint): + _log = app.srv_config.logger + _log.info('At the "{}" endpoint'.format(endpoint.name)) + + http_info = { + "headers": {k: v for k, v in request.headers.items(lower=True) if k not in IGNORE}, + "method": request.method, + "url": request.url, + # name is not unique + "cookie": [{"name": k, "value": v} for k, v in request.cookies.items()] + } + + if request.method == 'GET': + try: + req_args = endpoint.parse_request(request.args.to_dict(), http_info=http_info) + except (InvalidClient, UnknownClient) as err: + _log.error(err) + return make_response(json.dumps({ + 'error': 'unauthorized_client', + 'error_description': str(err) + }), 400) + except Exception as err: + _log.error(err) + return make_response(json.dumps({ + 'error': 'invalid_request', + 'error_description': str(err) + }), 400) + else: + if request.data: + if isinstance(request.data, str): + req_args = request.data + else: + req_args = request.data.decode() + else: + req_args = dict([(k, v) for k, v in request.form.items()]) + try: + req_args = endpoint.parse_request(req_args, http_info=http_info) + except Exception as err: + _log.error(err) + err_msg = ResponseMessage(error='invalid_request', error_description=str(err)) + return make_response(err_msg.to_json(), 400) + + _log.info('request: {}'.format(req_args)) + if isinstance(req_args, ResponseMessage) and 'error' in req_args: + return make_response(req_args.to_json(), 400) + + try: + if isinstance(endpoint, Token): + args = endpoint.process_request(AccessTokenRequest(**req_args), http_info=http_info) + else: + args = endpoint.process_request(req_args, http_info=http_info) + except Exception as err: + message = traceback.format_exception(*sys.exc_info()) + _log.error(message) + err_msg = ResponseMessage(error='invalid_request', error_description=str(err)) + return make_response(err_msg.to_json(), 400) + + _log.info('Response args: {}'.format(args)) + + if 'redirect_location' in args: + return redirect(args['redirect_location']) + if 'http_response' in args: + return make_response(args['http_response'], 200) + + response = do_response(endpoint, req_args, **args) + return response diff --git a/example/flask_op/config.json b/example/flask_op/config.json index 1f0fb783..e3fe080d 100644 --- a/example/flask_op/config.json +++ b/example/flask_op/config.json @@ -116,6 +116,20 @@ "implicit", "urn:ietf:params:oauth:grant-type:jwt-bearer", "refresh_token" + ], + "request_object_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512", + "PS256", + "PS384", + "PS512" ] }, "cookie_handler": { diff --git a/example/flask_op/config.yaml b/example/flask_op/config.yaml deleted file mode 100644 index b79a5fc9..00000000 --- a/example/flask_op/config.yaml +++ /dev/null @@ -1,268 +0,0 @@ -logging: - version: 1 - root: - handlers: - - default - - console - level: DEBUG - loggers: - bobcat_idp: - level: DEBUG - handlers: - default: - class: logging.FileHandler - filename: 'debug.log' - formatter: default - console: - class: logging.StreamHandler - stream: 'ext://sys.stdout' - formatter: default - formatters: - default: - format: '%(asctime)s %(name)s %(levelname)s %(message)s' - -port: &port 5000 -domain: &domain 192.168.1.158 -server_name: '{domain}:{port}' -base_url: &base_url 'https://{domain}:{port}' - -key_def: &key_def - - - type: RSA - use: - - sig - - - type: EC - crv: "P-256" - use: - - sig - - -op: - server_info: - issuer: *base_url - httpc_params: - verify: False - capabilities: - subject_types_supported: - - public - - pairwise - grant_types_supported: - - authorization_code - - implicit - - urn:ietf:params:oauth:grant-type:jwt-bearer - - refresh_token - template_dir: templates - token_handler_args: - jwks_def: - private_path: 'private/token_jwks.json' - read_only: False - key_defs: - - - type: oct - bytes: 24 - use: - - enc - kid: code - - - type: oct - bytes: 24 - use: - - enc - kid: refresh - code: - lifetime: 600 - id_token: - class: oidcop.token.id_token.IDToken - kwargs: - base_claims: - email: - essential: True - email_verified: - essential: True - token: - class: oidcop.token.jwt_token.JWTToken - lifetime: 3600 - add_claims: - - email - - email_verified - - phone_number - - phone_number_verified - add_claims_by_scope: True - aud: - - https://example.org/appl - refresh: - lifetime: 86400 - keys: - private_path: "private/jwks.json" - key_defs: *key_def - public_path: 'static/jwks.json' - read_only: False - # otherwise OP metadata will have jwks_uri: https://127.0.0.1:5000/None! - uri_path: 'static/jwks.json' - endpoint: - webfinger: - path: '.well-known/webfinger' - class: oidcop.oidc.discovery.Discovery - kwargs: - client_authn_method: null - provider_info: - path: ".well-known/openid-configuration" - class: oidcop.oidc.provider_config.ProviderConfiguration - kwargs: - client_authn_method: null - registration: - path: registration - class: oidcop.oidc.registration.Registration - kwargs: - client_authn_method: null - client_secret_expiration_time: 432000 - registration_api: - path: registration_api - class: oidcop.oidc.read_registration.RegistrationRead - kwargs: - client_authn_method: - - bearer_header - introspection: - path: introspection - class: oidcop.oauth2.introspection.Introspection - kwargs: - client_authn_method: - - client_secret_post - release: - - username - authorization: - path: authorization - class: oidcop.oidc.authorization.Authorization - kwargs: - client_authn_method: null - claims_parameter_supported: True - request_parameter_supported: True - 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 - response_modes_supported: - - query - - fragment - - form_post - token: - path: token - class: oidcop.oidc.token.Token - kwargs: - client_authn_method: - - client_secret_post - - client_secret_basic - - client_secret_jwt - - private_key_jwt - userinfo: - path: userinfo - class: oidcop.oidc.userinfo.UserInfo - kwargs: - claim_types_supported: - - normal - - aggregated - - distributed - end_session: - path: session - class: oidcop.oidc.session.Session - kwargs: - logout_verify_url: verify_logout - post_logout_uri_path: post_logout - signing_alg: "ES256" - frontchannel_logout_supported: True - frontchannel_logout_session_supported: True - backchannel_logout_supported: True - backchannel_logout_session_supported: True - check_session_iframe: 'check_session_iframe' - userinfo: - class: oidcop.user_info.UserInfo - kwargs: - db_file: users.json - authentication: - user: - acr: urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword - class: oidcop.user_authn.user.UserPassJinja2 - kwargs: - verify_endpoint: 'verify/user' - template: user_pass.jinja2 - db: - class: oidcop.util.JSONDictDB - kwargs: - filename: passwd.json - page_header: "Testing log in" - submit_btn: "Get me in!" - user_label: "Nickname" - passwd_label: "Secret sauce" - #anon: - #acr: oidcop.user_authn.authn_context.UNSPECIFIED - #class: oidcop.user_authn.user.NoAuthn - #kwargs: - #user: diana - cookie_handler: - class: oidcop.cookie_handler.CookieHandler - kwargs": - keys: - private_path: "private/cookie_jwks.json" - key_defs: - - - type: OCT - kid: enc - use: - - enc - - - type: OCT - kid: sig - use: - - sig - read_only: false - name: - session: "oidc_op" - register: "oidc_op_rp" - session_management: "sman" - login_hint2acrs: - class: oidcop.login_hint.LoginHint2Acrs - kwargs: - scheme_map: - email: - - urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword - - # this adds PKCE support as mandatory - disable it if needed (essential: False) - add_on: - pkce: - function: oidcop.oidc.add_on.pkce.add_pkce_support - kwargs: - essential: false - code_challenge_method: - #plain - S256 - S384 - S512 - - claims: - function: oidcop.oidc.add_on.custom_scopes.add_custom_scopes - kwargs: - research_and_scholarship: - - name - - given_name - - family_name - - email - - email_verified - - sub - - iss - - eduperson_scoped_affiliation - -webserver: - server_cert: 'certs/89296913_127.0.0.1.cert' - server_key: 'certs/89296913_127.0.0.1.key' - ca_bundle: null - verify_user: false - port: *port - domain: 0.0.0.0 - debug: true diff --git a/example/flask_op/seed.txt b/example/flask_op/seed.txt deleted file mode 100644 index e5bd6105..00000000 --- a/example/flask_op/seed.txt +++ /dev/null @@ -1 +0,0 @@ -5gRv9mEEqMwO1xwkNblGXNsvbMVMghbaZbCAYMzBYOxj2Sji \ No newline at end of file diff --git a/example/flask_op/views.py b/example/flask_op/views.py index d17b7347..648c3de6 100644 --- a/example/flask_op/views.py +++ b/example/flask_op/views.py @@ -304,9 +304,9 @@ def check_session_iframe(): return 'error' return 'OK' - current_app.logger.debug( - 'check_session_iframe: {}'.format(req_args)) + current_app.logger.debug('check_session_iframe: {}'.format(req_args)) doc = open('templates/check_session_iframe.html').read() + current_app.logger.debug(f"check_session_iframe response: {doc}") return doc diff --git a/example/flask_op/yaml_to_json.py b/example/flask_op/yaml_to_json.py new file mode 100755 index 00000000..4f7052d4 --- /dev/null +++ b/example/flask_op/yaml_to_json.py @@ -0,0 +1,11 @@ +#! /usr/bin/env python3 +import json +import sys + +import yaml + +"""Load a YAML configuration file.""" +with open(sys.argv[1], "rt", encoding='utf-8') as file: + config_dict = yaml.safe_load(file) + +print(json.dumps(config_dict)) diff --git a/pyproject.toml b/pyproject.toml index 7564b0ee..f63cfeb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [metadata] name = "oidcop" -version = "2.1.0" +version = "2.3.0" author = "Roland Hedberg" author_email = "roland@catalogix.se" description = "Python implementation of an OAuth2 AS and an OIDC Provider" diff --git a/setup.py b/setup.py index d707fd8a..47b7b04b 100644 --- a/setup.py +++ b/setup.py @@ -72,8 +72,7 @@ def run_tests(self): "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries :: Python Modules"], install_requires=[ - "oidcmsg==1.4.0", - "cryptojwt==1.5.2", + "oidcmsg==1.5.3", "pyyaml", "jinja2>=2.11.3", "responses>=0.13.0" diff --git a/src/oidcop/__init__.py b/src/oidcop/__init__.py index 29c1f2a8..de2c3aa2 100644 --- a/src/oidcop/__init__.py +++ b/src/oidcop/__init__.py @@ -1,6 +1,6 @@ import secrets -__version__ = "2.2.1" +__version__ = "2.3.0" DEF_SIGN_ALG = { "id_token": "RS256", diff --git a/src/oidcop/authn_event.py b/src/oidcop/authn_event.py index 0b5bf0e8..6b7bae0a 100644 --- a/src/oidcop/authn_event.py +++ b/src/oidcop/authn_event.py @@ -2,7 +2,7 @@ from oidcmsg.message import SINGLE_OPTIONAL_STRING from oidcmsg.message import SINGLE_REQUIRED_STRING from oidcmsg.message import Message -from oidcmsg.time_util import time_sans_frac +from oidcmsg.time_util import utc_time_sans_frac DEFAULT_AUTHN_EXPIRES_IN = 3600 @@ -20,10 +20,10 @@ def is_valid(self, now=0): if now: return self["valid_until"] > now else: - return self["valid_until"] > time_sans_frac() + return self["valid_until"] > utc_time_sans_frac() def expires_in(self): - return self["valid_until"] - time_sans_frac() + return self["valid_until"] - utc_time_sans_frac() def create_authn_event( @@ -59,7 +59,7 @@ def create_authn_event( if _ts: args["authn_time"] = _ts else: - args["authn_time"] = time_sans_frac() + args["authn_time"] = utc_time_sans_frac() if valid_until: args["valid_until"] = valid_until diff --git a/src/oidcop/authz/__init__.py b/src/oidcop/authz/__init__.py index a22a11bb..72b69797 100755 --- a/src/oidcop/authz/__init__.py +++ b/src/oidcop/authz/__init__.py @@ -57,7 +57,10 @@ def usage_rules_for(self, client_id, token_type): return {} def __call__( - self, session_id: str, request: Union[dict, Message], resources: Optional[list] = None, + self, + session_id: str, + request: Union[dict, Message], + resources: Optional[list] = None, ) -> Grant: session_info = self.server_get("endpoint_context").session_manager.get_session_info( session_id=session_id, grant=True @@ -93,7 +96,10 @@ def __call__( class Implicit(AuthzHandling): def __call__( - self, session_id: str, request: Union[dict, Message], resources: Optional[list] = None, + self, + session_id: str, + request: Union[dict, Message], + resources: Optional[list] = None, ) -> Grant: args = self.grant_config.copy() grant = self.server_get("endpoint_context").session_manager.get_grant(session_id=session_id) diff --git a/src/oidcop/client_authn.py b/src/oidcop/client_authn.py index 7678419a..8c540b01 100755 --- a/src/oidcop/client_authn.py +++ b/src/oidcop/client_authn.py @@ -141,8 +141,7 @@ def verify(self, request, **kwargs): class BearerHeader(ClientSecretBasic): - """ - """ + """""" tag = "bearer_header" @@ -348,7 +347,7 @@ def verify_client( authorization_token = None auth_info = {} - _methods = getattr(endpoint, 'client_authn_method', []) + _methods = getattr(endpoint, "client_authn_method", []) for _method in _methods: if _method is None: @@ -356,7 +355,9 @@ def verify_client( if _method.is_usable(request, authorization_token): try: auth_info = _method.verify( - request=request, authorization_token=authorization_token, endpoint=endpoint, + request=request, + authorization_token=authorization_token, + endpoint=endpoint, ) except Exception as err: logger.warning("Verifying auth using {} failed: {}".format(_method.tag, err)) diff --git a/src/oidcop/configure.py b/src/oidcop/configure.py index 5f51bac9..2b9cc6d9 100755 --- a/src/oidcop/configure.py +++ b/src/oidcop/configure.py @@ -24,7 +24,7 @@ "private_path", "public_path", "db_file", - "jwks_file" + "jwks_file", ] OP_DEFAULT_CONFIG = { @@ -48,8 +48,11 @@ ], "read_only": False, }, - "name": {"session": "oidc_op", "register": "oidc_op_rp", - "session_management": "sman", }, + "name": { + "session": "oidc_op", + "register": "oidc_op_rp", + "session_management": "sman", + }, }, }, "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, @@ -59,13 +62,17 @@ "grant_config": { "usage_rules": { "authorization_code": { - "supports_minting": ["access_token", "refresh_token", "id_token", ], + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], "max_usage": 1, }, "access_token": {}, "refresh_token": { "supports_minting": ["access_token", "refresh_token"], - "expires_in": -1 + "expires_in": -1, }, }, "expires_in": 43200, @@ -78,8 +85,14 @@ "token_handler_args": { "jwks_file": "private/token_jwks.json", "code": {"kwargs": {"lifetime": 600}}, - "token": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 3600}, }, - "refresh": {"class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 86400}, }, + "token": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": {"lifetime": 3600}, + }, + "refresh": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": {"lifetime": 86400}, + }, "id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}}, }, "scopes_to_claims": SCOPE2CLAIMS, @@ -87,7 +100,8 @@ AS_DEFAULT_CONFIG = copy.deepcopy(OP_DEFAULT_CONFIG) AS_DEFAULT_CONFIG["claims_interface"] = { - "class": "oidcop.session.claims.OAuth2ClaimsInterface", "kwargs": {} + "class": "oidcop.session.claims.OAuth2ClaimsInterface", + "kwargs": {}, } @@ -128,13 +142,13 @@ def set_domain_and_port(conf: dict, uris: List[str], domain: str, port: int): def create_from_config_file( - cls, - filename: str, - base_path: str = "", - entity_conf: Optional[List[dict]] = None, - file_attributes: Optional[List[str]] = None, - domain: Optional[str] = "", - port: Optional[int] = 0, + cls, + filename: str, + base_path: str = "", + entity_conf: Optional[List[dict]] = None, + file_attributes: Optional[List[str]] = None, + domain: Optional[str] = "", + port: Optional[int] = 0, ): if filename.endswith(".yaml"): """Load configuration as YAML""" @@ -166,7 +180,10 @@ class Base(dict): parameter = {} def __init__( - self, conf: Dict, base_path: str = "", file_attributes: Optional[List[str]] = None, + self, + conf: Dict, + base_path: str = "", + file_attributes: Optional[List[str]] = None, ): dict.__init__(self) @@ -182,12 +199,12 @@ def __getattr__(self, item): def __setattr__(self, key, value): if key in self: - raise KeyError('{} has already been set'.format(key)) + raise KeyError("{} has already been set".format(key)) super(Base, self).__setitem__(key, value) def __setitem__(self, key, value): if key in self: - raise KeyError('{} has already been set'.format(key)) + raise KeyError("{} has already been set".format(key)) super(Base, self).__setitem__(key, value) @@ -214,13 +231,13 @@ class EntityConfiguration(Base): } def __init__( - self, - conf: Dict, - base_path: Optional[str] = "", - entity_conf: Optional[List[dict]] = None, - domain: Optional[str] = "", - port: Optional[int] = 0, - file_attributes: Optional[List[str]] = None, + self, + conf: Dict, + base_path: Optional[str] = "", + entity_conf: Optional[List[dict]] = None, + domain: Optional[str] = "", + port: Optional[int] = 0, + file_attributes: Optional[List[str]] = None, ): conf = copy.deepcopy(conf) @@ -240,19 +257,20 @@ def __init__( if not _val: if key in self.default_config: _val = copy.deepcopy(self.default_config[key]) - self.format(_val, base_path=base_path, file_attributes=file_attributes, - domain=domain, port=port) + self.format( + _val, + base_path=base_path, + file_attributes=file_attributes, + domain=domain, + port=port, + ) else: continue if key not in DEFAULT_EXTENDED_CONF: - logger.warning( - f"{key} not seems to be a valid configuration parameter" - ) + logger.warning(f"{key} not seems to be a valid configuration parameter") elif not _val: - logger.warning( - f"{key} not configured, using default configuration values" - ) + logger.warning(f"{key} not configured, using default configuration values") if key == "template_dir": _val = os.path.abspath(_val) @@ -314,38 +332,45 @@ def __init__( port=port, file_attributes=file_attributes, ) - scopes_to_claims = self.scopes_to_claims + self.scopes_to_claims class ASConfiguration(EntityConfiguration): "Authorization server configuration" def __init__( - self, - conf: Dict, - base_path: Optional[str] = "", - entity_conf: Optional[List[dict]] = None, - domain: Optional[str] = "", - port: Optional[int] = 0, - file_attributes: Optional[List[str]] = None, + self, + conf: Dict, + base_path: Optional[str] = "", + entity_conf: Optional[List[dict]] = None, + domain: Optional[str] = "", + port: Optional[int] = 0, + file_attributes: Optional[List[str]] = None, ): - EntityConfiguration.__init__(self, conf=conf, base_path=base_path, - entity_conf=entity_conf, domain=domain, port=port, - file_attributes=file_attributes) + EntityConfiguration.__init__( + self, + conf=conf, + base_path=base_path, + entity_conf=entity_conf, + domain=domain, + port=port, + file_attributes=file_attributes, + ) class Configuration(Base): """Server Configuration""" + uris = ["issuer", "base_url"] def __init__( - self, - conf: Dict, - entity_conf: Optional[List[dict]] = None, - base_path: str = "", - file_attributes: Optional[List[str]] = None, - domain: Optional[str] = "", - port: Optional[int] = 0, + self, + conf: Dict, + entity_conf: Optional[List[dict]] = None, + base_path: str = "", + file_attributes: Optional[List[str]] = None, + domain: Optional[str] = "", + port: Optional[int] = 0, ): Base.__init__(self, conf, base_path, file_attributes) @@ -415,13 +440,17 @@ def __init__( "grant_config": { "usage_rules": { "authorization_code": { - "supports_minting": ["access_token", "refresh_token", "id_token", ], + "supports_minting": [ + "access_token", + "refresh_token", + "id_token", + ], "max_usage": 1, }, "access_token": {}, "refresh_token": { "supports_minting": ["access_token", "refresh_token"], - "expires_in": -1 + "expires_in": -1, }, }, "expires_in": 43200, @@ -435,7 +464,10 @@ def __init__( "kwargs": { "verify_endpoint": "verify/user", "template": "user_pass.jinja2", - "db": {"class": "oidcop.util.JSONDictDB", "kwargs": {"filename": "passwd.json"}, }, + "db": { + "class": "oidcop.util.JSONDictDB", + "kwargs": {"filename": "passwd.json"}, + }, "page_header": "Testing log in", "submit_btn": "Get me in!", "user_label": "Nickname", @@ -463,8 +495,11 @@ def __init__( ], "read_only": False, }, - "name": {"session": "oidc_op", "register": "oidc_op_rp", - "session_management": "sman", }, + "name": { + "session": "oidc_op", + "register": "oidc_op_rp", + "session_management": "sman", + }, }, }, "endpoint": { @@ -481,7 +516,10 @@ def __init__( "registration": { "path": "registration", "class": "oidcop.oidc.registration.Registration", - "kwargs": {"client_authn_method": None, "client_secret_expiration_time": 432000, }, + "kwargs": { + "client_authn_method": None, + "client_secret_expiration_time": 432000, + }, }, "registration_api": { "path": "registration_api", @@ -491,7 +529,10 @@ def __init__( "introspection": { "path": "introspection", "class": "oidcop.oauth2.introspection.Introspection", - "kwargs": {"client_authn_method": ["client_secret_post"], "release": ["username"], }, + "kwargs": { + "client_authn_method": ["client_secret_post"], + "release": ["username"], + }, }, "authorization": { "path": "authorization", @@ -562,7 +603,8 @@ def __init__( "class": "oidcop.login_hint.LoginHint2Acrs", "kwargs": { "scheme_map": { - "email": ["urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"]} + "email": ["urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"] + } }, }, "template_dir": "templates", @@ -583,7 +625,10 @@ def __init__( }, "refresh": { "class": "oidcop.token.jwt_token.JWTToken", - "kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], }, + "kwargs": { + "lifetime": 3600, + "aud": ["https://example.org/appl"], + }, }, "id_token": { "class": "oidcop.token.id_token.IDToken", @@ -595,24 +640,20 @@ def __init__( }, }, }, - "userinfo": {"class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": "users.json"}, }, + "userinfo": { + "class": "oidcop.user_info.UserInfo", + "kwargs": {"db_file": "users.json"}, + }, "scopes_to_claims": SCOPE2CLAIMS, "session_params": { - "password": "ses_key", - "salt": "ses_salt", - "sub_func": { - "public": { - "class": "oidcop.session.manager.PublicID", - "kwargs": { - "salt": "mysalt" - } + "password": "ses_key", + "salt": "ses_salt", + "sub_func": { + "public": {"class": "oidcop.session.manager.PublicID", "kwargs": {"salt": "mysalt"}}, + "pairwise": { + "class": "oidcop.session.manager.PairWiseID", + "kwargs": {"salt": "mysalt"}, + }, }, - "pairwise": { - "class": "oidcop.session.manager.PairWiseID", - "kwargs": { - "salt": "mysalt" - } - } - } }, } diff --git a/src/oidcop/cookie_handler.py b/src/oidcop/cookie_handler.py index 7812f825..ea2549b5 100755 --- a/src/oidcop/cookie_handler.py +++ b/src/oidcop/cookie_handler.py @@ -37,7 +37,7 @@ def __init__( keys: Optional[dict] = None, sign_alg: [str] = "SHA256", name: Optional[dict] = None, - **kwargs + **kwargs, ): if keys: @@ -79,12 +79,12 @@ def __init__( self.name = name self.flags = kwargs.get( - 'flags', + "flags", { - "samesite": "None", - "httponly": True, - "secure": True, - } + "samesite": "None", + "httponly": True, + "secure": True, + }, ) def _sign_enc_payload(self, payload: str, timestamp: Optional[Union[int, str]] = 0): @@ -153,7 +153,9 @@ def _ver_dec_content(self, parts): mac = base64.b64decode(b64_mac) verifier = HMACSigner(algorithm=self.sign_alg) if verifier.verify( - payload.encode("utf-8") + timestamp.encode("utf-8"), mac, self.sign_key.key, + payload.encode("utf-8") + timestamp.encode("utf-8"), + mac, + self.sign_key.key, ): return payload, timestamp else: @@ -194,7 +196,7 @@ def make_cookie_content( typ: Optional[str] = "", timestamp: Optional[Union[int, str]] = "", max_age: Optional[int] = 0, - **kwargs + **kwargs, ) -> dict: """ Create and return information to put in a cookie @@ -228,7 +230,7 @@ def make_cookie_content( elif max_age: content["max-age"] = epoch_in_a_while(seconds=max_age) - for k,v in self.flags.items(): + for k, v in self.flags.items(): content[k] = v return content @@ -253,7 +255,7 @@ def parse_cookie(self, name: str, cookies: List[dict]) -> Optional[List[dict]]: LOGGER.debug("Looking for '{}' cookies".format(name)) res = [] for _cookie in cookies: - LOGGER.debug('Cookie: {}'.format(_cookie)) + LOGGER.debug("Cookie: {}".format(_cookie)) if "name" in _cookie and _cookie["name"] == name: _content = self._ver_dec_content(_cookie["value"].split("|")) if _content: diff --git a/src/oidcop/endpoint.py b/src/oidcop/endpoint.py index f5ea2bf8..854b85ae 100755 --- a/src/oidcop/endpoint.py +++ b/src/oidcop/endpoint.py @@ -1,6 +1,5 @@ import logging from typing import Callable -from typing import List from typing import Optional from typing import Union from urllib.parse import urlparse @@ -9,6 +8,7 @@ from oidcmsg.exception import MissingRequiredValue from oidcmsg.message import Message from oidcmsg.oauth2 import ResponseMessage +from oidcmsg.oidc import RegistrationRequest from oidcop import sanitize from oidcop.client_authn import client_auth_setup @@ -128,6 +128,10 @@ def __init__(self, server_get: Callable, **kwargs): self.allowed_targets = [self.name] self.client_verification_method = [] + def process_verify_error(self, exception): + _error = "invalid_request" + return self.error_cls(error=_error, error_description="%s" % exception) + def parse_request( self, request: Union[Message, dict, str], http_info: Optional[dict] = None, **kwargs ): @@ -185,13 +189,21 @@ def parse_request( try: req.verify(keyjar=keyjar, opponent_id=_client_id) except (MissingRequiredAttribute, ValueError, MissingRequiredValue) as err: - return self.error_cls(error="invalid_request", error_description="%s" % err) + return self.process_verify_error(err) + _error = "invalid_request" + if isinstance(err, ValueError) and self.request_cls == RegistrationRequest: + if len(err.args) > 1: + if err.args[1] == "initiate_login_uri": + _error = "invalid_client_metadata" + + return self.error_cls(error=_error, error_description="%s" % err) LOGGER.info("Parsed and verified request: %s" % sanitize(req)) # Do any endpoint specific parsing - return self.do_post_parse_request(request=req, client_id=_client_id, http_info=http_info, - **kwargs) + return self.do_post_parse_request( + request=req, client_id=_client_id, http_info=http_info, **kwargs + ) def get_client_id_from_token( self, diff --git a/src/oidcop/endpoint_context.py b/src/oidcop/endpoint_context.py index 51629b67..fb513a58 100755 --- a/src/oidcop/endpoint_context.py +++ b/src/oidcop/endpoint_context.py @@ -15,7 +15,6 @@ from oidcop.configure import OPConfiguration from oidcop.scopes import SCOPE2CLAIMS from oidcop.scopes import Scopes -from oidcop.session.claims import STANDARD_CLAIMS from oidcop.session.manager import SessionManager from oidcop.template_handler import Jinja2TemplateHandler from oidcop.util import get_http_params diff --git a/src/oidcop/logging.py b/src/oidcop/logging.py index d04c577e..10dfdc1e 100755 --- a/src/oidcop/logging.py +++ b/src/oidcop/logging.py @@ -17,7 +17,9 @@ def configure_logging( - debug: Optional[bool] = False, config: Optional[dict] = None, filename: Optional[str] = "", + debug: Optional[bool] = False, + config: Optional[dict] = None, + filename: Optional[str] = "", ) -> logging.Logger: """Configure logging""" diff --git a/src/oidcop/login_hint.py b/src/oidcop/login_hint.py index cd64a3c9..904a64a2 100644 --- a/src/oidcop/login_hint.py +++ b/src/oidcop/login_hint.py @@ -24,6 +24,7 @@ class LoginHint2Acrs(object): """ OIDC Login hint support """ + def __init__(self, scheme_map, server_get=None): self.scheme_map = scheme_map self.server_get = server_get diff --git a/src/oidcop/oauth2/authorization.py b/src/oidcop/oauth2/authorization.py index 2d13ad50..86b7ce82 100755 --- a/src/oidcop/oauth2/authorization.py +++ b/src/oidcop/oauth2/authorization.py @@ -45,11 +45,18 @@ # For the time being. This is JAR specific and should probably be configurable. ALG_PARAMS = { - "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported", ], - "enc_alg": ["request_object_encryption_alg", - "request_object_encryption_alg_values_supported", ], - "enc_enc": ["request_object_encryption_enc", - "request_object_encryption_enc_values_supported", ], + "sign": [ + "request_object_signing_alg", + "request_object_signing_alg_values_supported", + ], + "enc_alg": [ + "request_object_encryption_alg", + "request_object_encryption_alg_values_supported", + ], + "enc_enc": [ + "request_object_encryption_enc", + "request_object_encryption_enc_values_supported", + ], } FORM_POST = """ @@ -83,10 +90,10 @@ def max_age(request): def verify_uri( - endpoint_context: EndpointContext, - request: Union[dict, Message], - uri_type: str, - client_id: Optional[str] = None, + endpoint_context: EndpointContext, + request: Union[dict, Message], + uri_type: str, + client_id: Optional[str] = None, ): """ A redirect URI @@ -116,20 +123,25 @@ def verify_uri( raise URIError("Contains fragment") (_base, _query) = split_uri(_redirect_uri) - # if _query: - # _query = parse_qs(_query) # Get the clients registered redirect uris client_info = endpoint_context.cdb.get(_cid) if client_info is None: raise KeyError("No such client") - redirect_uris = client_info.get("{}s".format(uri_type)) + redirect_uris = client_info.get(f"{uri_type}s") + if redirect_uris is None: - raise ValueError(f"No registered {uri_type} for {_cid}") + raise RedirectURIError(f"No registered {uri_type} for {_cid}") else: match = False - for regbase, rquery in redirect_uris: + for _item in redirect_uris: + if isinstance(_item, str): + regbase = _item + rquery = {} + else: + regbase, rquery = _item + # The URI MUST exactly match one of the Redirection URI if _base == regbase: # every registered query component must exist in the uri @@ -177,7 +189,7 @@ def join_query(base, query): def get_uri(endpoint_context, request, uri_type): - """ verify that the redirect URI is reasonable. + """verify that the redirect URI is reasonable. :param endpoint_context: An EndpointContext instance :param request: The Authorization request @@ -208,7 +220,10 @@ def get_uri(endpoint_context, request, uri_type): def authn_args_gather( - request: Union[AuthorizationRequest, dict], authn_class_ref: str, cinfo: dict, **kwargs, + request: Union[AuthorizationRequest, dict], + authn_class_ref: str, + cinfo: dict, + **kwargs, ): """ Gather information to be used by the authentication method @@ -251,9 +266,7 @@ def check_unknown_scopes_policy(request_info, client_id, endpoint_context): return scope = request_info["scope"] - filtered_scopes = set( - endpoint_context.scopes_handler.filter_scopes(scope, client_id=client_id) - ) + filtered_scopes = set(endpoint_context.scopes_handler.filter_scopes(scope, client_id=client_id)) scopes = set(scope) # this prevents that authz would be released for unavailable scopes if scopes != filtered_scopes: @@ -377,10 +390,16 @@ def _do_request_uri(self, request, client_id, endpoint_context, **kwargs): ) if _ver_request.jwe_header is not None: self.allowed_request_algorithms( - client_id, endpoint_context, _ver_request.jws_header.get("alg"), "enc_alg", + client_id, + endpoint_context, + _ver_request.jws_header.get("alg"), + "enc_alg", ) self.allowed_request_algorithms( - client_id, endpoint_context, _ver_request.jws_header.get("enc"), "enc_enc", + client_id, + endpoint_context, + _ver_request.jws_header.get("enc"), + "enc_enc", ) # The protected info overwrites the non-protected for k, v in _ver_request.items(): @@ -404,18 +423,18 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs): """ if not request: logger.debug("No AuthzRequest") - return self.authentication_error_response(request, - error="invalid_request", - error_description="Can not parse AuthzRequest" - ) + return self.authentication_error_response( + request, error="invalid_request", error_description="Can not parse AuthzRequest" + ) request = self.filter_request(endpoint_context, request) _cinfo = endpoint_context.cdb.get(client_id) if not _cinfo: logger.error("Client ID ({}) not in client database".format(request["client_id"])) - return self.authentication_error_response(request, error="unauthorized_client", - error_description="unknown client") + return self.authentication_error_response( + request, error="unauthorized_client", error_description="unknown client" + ) # Is the asked for response_type among those that are permitted if not self.verify_response_type(request, _cinfo): @@ -452,9 +471,7 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs): try: res = pick_auth(_context, request) except Exception as exc: - logger.exception( - f"An error occurred while picking the authN broker: {exc}" - ) + logger.exception(f"An error occurred while picking the authN broker: {exc}") if res: return res else: @@ -468,7 +485,11 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs): def create_session(self, request, user_id, acr, time_stamp, authn_method): _context = self.server_get("endpoint_context") _mngr = _context.session_manager - authn_event = create_authn_event(user_id, authn_info=acr, time_stamp=time_stamp, ) + authn_event = create_authn_event( + user_id, + authn_info=acr, + time_stamp=time_stamp, + ) _exp_in = authn_method.kwargs.get("expires_in") if _exp_in and "valid_until" in authn_event: authn_event["valid_until"] = utc_time_sans_frac() + _exp_in @@ -494,15 +515,26 @@ def _login_required_error(self, redirect_uri, request): logger.debug("Login required error: {}".format(_res)) return _res + def _unwrap_identity(self, identity): + if isinstance(identity, dict): + try: + _id = b64d(as_bytes(identity["uid"])) + except BadSyntax: + return identity + else: + _id = b64d(as_bytes(identity)) + + return json.loads(as_unicode(_id)) + def setup_auth( - self, - request: Optional[Union[Message, dict]], - redirect_uri: str, - cinfo: dict, - cookie: List[dict] = None, - acr: str = None, - **kwargs, - ): + self, + request: Optional[Union[Message, dict]], + redirect_uri: str, + cinfo: dict, + cookie: List[dict] = None, + acr: str = None, + **kwargs, + ) -> dict: """ :param request: The authorization/authentication request @@ -527,7 +559,7 @@ def setup_auth( _max_age = 0 else: _max_age = max_age(request) - logger.debug(f'Max age: {_max_age}') + logger.debug(f"Max age: {_max_age}") identity, _ts = authn.authenticated_as( client_id, cookie, authorization=_auth_info, max_age=_max_age ) @@ -544,18 +576,16 @@ def setup_auth( _ts = 0 else: if identity: - try: # If identity['uid'] is in fact a base64 encoded JSON string - _id = b64d(as_bytes(identity["uid"])) - except BadSyntax: - pass - else: - identity = json.loads(as_unicode(_id)) - + identity = self._unwrap_identity(identity) + _sid = identity.get("sid") + if _sid: try: - _csi = _context.session_manager[identity.get("sid")] + _csi = _context.session_manager[_sid] except Revoked: + logger.debug("Authentication session revoked!!") identity = None else: + logger.debug(f"Session info type: {_csi.__class__.__name__}") if _csi.is_active() is False: identity = None @@ -574,7 +604,7 @@ def setup_auth( else: return {"function": authn, "args": authn_args} else: - logger.info("Active authentication") + logger.info(f"Active authentication: {identity}") if re_authenticate(request, authn): # demand re-authentication return {"function": authn, "args": authn_args} @@ -626,12 +656,12 @@ def aresp_check(self, aresp, request): return "" def response_mode( - self, - request: Union[dict, AuthorizationRequest], - response_args: Optional[AuthorizationResponse] = None, - return_uri: Optional[str] = "", - fragment_enc: Optional[bool] = None, - **kwargs, + self, + request: Union[dict, AuthorizationRequest], + response_args: Optional[Union[dict, AuthorizationResponse]] = None, + return_uri: Optional[str] = "", + fragment_enc: Optional[bool] = None, + **kwargs, ) -> dict: resp_mode = request["response_mode"] if resp_mode == "form_post": @@ -639,9 +669,21 @@ def response_mode( _args = response_args.to_dict() else: _args = response_args - msg = FORM_POST.format(inputs=inputs(_args), action=return_uri, ) + + if "error" in _args: + if not return_uri: + return_uri = _args["return_uri"] + del _args["return_uri"] + if "return_type" in _args: + del _args["return_type"] + + msg = FORM_POST.format(inputs=inputs(_args), action=return_uri) kwargs.update( - {"response_msg": msg, "content_type": "text/html", "response_placement": "body", } + { + "response_msg": msg, + "content_type": "text/html", + "response_placement": "body", + } ) elif resp_mode == "fragment": if fragment_enc is False: @@ -662,12 +704,19 @@ def response_mode( return kwargs def error_response(self, response_info, request, error, error_description): - resp = self.authentication_error_response(request, - error=error, - error_description=str(error_description)) + resp = self.authentication_error_response( + request, error=error, error_description=str(error_description) + ) response_info["response_args"] = resp return response_info + def error_by_response_mode(self, response_info, request, error, error_description): + response_info = self.error_response(response_info, request, error, error_description) + if "return_uri" not in response_info: + response_info["return_uri"] = request["redirect_uri"] + response_info = self.response_mode(request, **response_info) + return response_info + def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict: """ :param request: @@ -702,7 +751,9 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict if "code" in request["response_type"]: _code = self.mint_token( - token_class="authorization_code", grant=grant, session_id=_sinfo["session_id"], + token_class="authorization_code", + grant=grant, + session_id=_sinfo["session_id"], ) aresp["code"] = _code.value handled_response_type.append("code") @@ -711,7 +762,9 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict if "token" in rtype: _access_token = self.mint_token( - token_class="access_token", grant=grant, session_id=_sinfo["session_id"], + token_class="access_token", + grant=grant, + session_id=_sinfo["session_id"], ) aresp["access_token"] = _access_token.value aresp["token_type"] = "Bearer" @@ -730,11 +783,15 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict elif {"id_token", "token"}.issubset(rtype): kwargs = {"access_token": _access_token.value} + if request["response_type"] == ["id_token"]: + kwargs["as_if"] = "userinfo" + try: id_token = self.mint_token( token_class="id_token", grant=grant, session_id=_sinfo["session_id"], + scope=request["scope"], **kwargs, ) # id_token = _context.idtoken.make(sid, **kwargs) @@ -754,7 +811,8 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict if not_handled: resp = self.authentication_error_response( request, - error="invalid_request", error_description="unsupported_response_type", + error="invalid_request", + error_description="unsupported_response_type", ) return {"response_args": resp, "fragment_enc": fragment_enc} @@ -786,8 +844,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** try: _mngr.set([user_id, client_id, grant_id], grant) except Exception as err: - return self.error_response(response_info, request, "server_error", - "{}".format(err.args)) + return self.error_response( + response_info, request, "server_error", "{}".format(err.args) + ) logger.debug("response type: %s" % request["response_type"]) @@ -799,8 +858,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** try: redirect_uri = get_uri(_context, request, "redirect_uri") except (RedirectURIError, ParameterError) as err: - return self.error_response(response_info, request, "invalid_request", - "{}".format(err.args)) + return self.error_response( + response_info, request, "invalid_request", "{}".format(err.args) + ) else: response_info["return_uri"] = redirect_uri @@ -812,8 +872,9 @@ def post_authentication(self, request: Union[dict, Message], session_id: str, ** try: response_info = self.response_mode(request, **response_info) except InvalidRequest as err: - return self.error_response(response_info, request, "invalid_request", - "{}".format(err.args)) + return self.error_response( + response_info, request, "invalid_request", "{}".format(err.args) + ) _cookie_info = _context.new_cookie( name=_context.cookie_handler.name["session"], @@ -837,40 +898,47 @@ def authz_part2(self, request, session_id, **kwargs): try: resp_info = self.post_authentication(request, session_id, **kwargs) except Exception as err: - return self.error_response({}, request, "server_error", err) + return self.error_by_response_mode({}, request, "server_error", err) _context = self.server_get("endpoint_context") + logger.debug(f"resp_info: {resp_info}") + if "check_session_iframe" in _context.provider_info: salt = rndstr() try: authn_event = _context.session_manager.get_authentication_event(session_id) except KeyError: - return self.error_response({}, request, "server_error", "No such session") + return self.error_by_response_mode({}, request, "server_error", "No such session") else: if authn_event.is_valid() is False: - return self.error_response({}, request, "server_error", - "Authentication has timed out") + return self.error_by_response_mode( + {}, request, "server_error", "Authentication has timed out" + ) _state = b64e(as_bytes(json.dumps({"authn_time": authn_event["authn_time"]}))) _session_cookie_content = _context.new_cookie( - name=_context.cookie_handler.name["session_management"], state=as_unicode(_state), + name=_context.cookie_handler.name["session_management"], + state=as_unicode(_state), ) opbs_value = _session_cookie_content["value"] + if "return_uri" in resp_info: + re_uri = resp_info["return_uri"] + else: + re_uri = request["redirect_uri"] + logger.debug( "compute_session_state: client_id=%s, origin=%s, opbs=%s, salt=%s", request["client_id"], - resp_info["return_uri"], + re_uri, opbs_value, salt, ) - _session_state = compute_session_state( - opbs_value, salt, request["client_id"], resp_info["return_uri"] - ) + _session_state = compute_session_state(opbs_value, salt, request["client_id"], re_uri) if _session_cookie_content: if "cookie" in resp_info: @@ -878,7 +946,8 @@ def authz_part2(self, request, session_id, **kwargs): else: resp_info["cookie"] = [_session_cookie_content] - resp_info["response_args"]["session_state"] = _session_state + if "response_args" in resp_info: + resp_info["response_args"]["session_state"] = _session_state # Mix-Up mitigation if "response_args" in resp_info: @@ -891,12 +960,12 @@ def do_request_user(self, request_info, **kwargs): return kwargs def process_request( - self, - request: Optional[Union[Message, dict]] = None, - http_info: Optional[dict] = None, - **kwargs, + self, + request: Optional[Union[Message, dict]] = None, + http_info: Optional[dict] = None, + **kwargs, ): - """ The AuthorizationRequest endpoint + """The AuthorizationRequest endpoint :param http_info: Information on the HTTP request :param request: The authorization request as a Message instance @@ -930,7 +999,10 @@ def process_request( info = self.setup_auth(request, request["redirect_uri"], cinfo, _my_cookies, **kwargs) if "error" in info: - return info + if "response_mode" in request: + return self.response_mode(request, info) + else: + return info _function = info.get("function") if not _function: diff --git a/src/oidcop/oauth2/token.py b/src/oidcop/oauth2/token.py index 768c1734..bfee6dff 100755 --- a/src/oidcop/oauth2/token.py +++ b/src/oidcop/oauth2/token.py @@ -3,13 +3,12 @@ from typing import Union from cryptojwt.jwe.exception import JWEException -from cryptojwt.jwt import utc_time_sans_frac from oidcmsg.message import Message from oidcmsg.oauth2 import AccessTokenResponse from oidcmsg.oauth2 import ResponseMessage from oidcmsg.oidc import RefreshAccessTokenRequest from oidcmsg.oidc import TokenErrorResponse -from oidcmsg.time_util import time_sans_frac +from oidcmsg.time_util import utc_time_sans_frac from oidcop import sanitize from oidcop.constant import DEFAULT_TOKEN_LIFETIME @@ -54,7 +53,7 @@ def _mint_token( based_on: Optional[SessionToken] = None, scope: Optional[list] = None, token_args: Optional[dict] = None, - token_type: Optional[str] = "" + token_type: Optional[str] = "", ) -> SessionToken: _context = self.endpoint.server_get("endpoint_context") _mngr = _context.session_manager @@ -90,7 +89,7 @@ def _mint_token( _exp_in = int(_exp_in) if _exp_in: - token.expires_at = time_sans_frac() + _exp_in + token.expires_at = utc_time_sans_frac() + _exp_in _context.session_manager.set(_context.session_manager.unpack_session_key(session_id), grant) @@ -296,7 +295,8 @@ def process_request(self, req: Union[Message, dict], **kwargs): token.register_usage() - if ("client_id" in req + if ( + "client_id" in req and req["client_id"] in _context.cdb and "revoke_refresh_on_issue" in _context.cdb[req["client_id"]] ): @@ -477,7 +477,9 @@ def process_request(self, request: Optional[Union[Message, dict]] = None, **kwar name=_context.cookie_handler.name["session"], sub=_session_info["grant"].sub, sid=_context.session_manager.session_key( - _session_info["user_id"], _session_info["user_id"], _session_info["grant"].id, + _session_info["user_id"], + _session_info["client_id"], + _session_info["grant"].id, ), ) diff --git a/src/oidcop/oidc/add_on/custom_scopes.py b/src/oidcop/oidc/add_on/custom_scopes.py index e3981e18..5d3d655a 100644 --- a/src/oidcop/oidc/add_on/custom_scopes.py +++ b/src/oidcop/oidc/add_on/custom_scopes.py @@ -19,7 +19,7 @@ def add_custom_scopes(endpoint, **kwargs): _scopes2claims = SCOPE2CLAIMS.copy() _scopes2claims.update(kwargs) _context = _endpoint.server_get("endpoint_context") - _context.scopes_handler.scopes_to_claims = _scopes2claims + _context.scopes_handler.set_scopes_mapping(_scopes2claims) pi = _context.provider_info _scopes = set(pi.get("scopes_supported", [])) diff --git a/src/oidcop/oidc/add_on/pkce.py b/src/oidcop/oidc/add_on/pkce.py index 6825c38c..3e7fa054 100644 --- a/src/oidcop/oidc/add_on/pkce.py +++ b/src/oidcop/oidc/add_on/pkce.py @@ -43,12 +43,11 @@ def post_authn_parse(request, client_id, endpoint_context, **kwargs): if "pkce_essential" in client: essential = client["pkce_essential"] else: - essential = endpoint_context.args["pkce"].get( - "essential", False - ) + essential = endpoint_context.args["pkce"].get("essential", False) if essential and "code_challenge" not in request: return AuthorizationErrorResponse( - error="invalid_request", error_description="Missing required code_challenge", + error="invalid_request", + error_description="Missing required code_challenge", ) if "code_challenge_method" not in request: @@ -93,7 +92,8 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs): :return: """ if isinstance( - request, (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest), + request, + (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest), ): return request @@ -109,13 +109,16 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs): if "code_challenge" in _authn_req: if "code_verifier" not in request: return TokenErrorResponse( - error="invalid_grant", error_description="Missing code_verifier", + error="invalid_grant", + error_description="Missing code_verifier", ) _method = _authn_req["code_challenge_method"] if not verify_code_challenge( - request["code_verifier"], _authn_req["code_challenge"], _method, + request["code_verifier"], + _authn_req["code_challenge"], + _method, ): return TokenErrorResponse(error="invalid_grant", error_description="PKCE check failed") diff --git a/src/oidcop/oidc/authorization.py b/src/oidcop/oidc/authorization.py index 670b42a7..fb4d73e8 100755 --- a/src/oidcop/oidc/authorization.py +++ b/src/oidcop/oidc/authorization.py @@ -43,9 +43,18 @@ def host_component(url): ALG_PARAMS = { - "sign": ["request_object_signing_alg", "request_object_signing_alg_values_supported",], - "enc_alg": ["request_object_encryption_alg", "request_object_encryption_alg_values_supported",], - "enc_enc": ["request_object_encryption_enc", "request_object_encryption_enc_values_supported",], + "sign": [ + "request_object_signing_alg", + "request_object_signing_alg_values_supported", + ], + "enc_alg": [ + "request_object_encryption_alg", + "request_object_encryption_alg_values_supported", + ], + "enc_enc": [ + "request_object_encryption_enc", + "request_object_encryption_enc_values_supported", + ], } diff --git a/src/oidcop/oidc/registration.py b/src/oidcop/oidc/registration.py index cc67dcd1..529664a9 100755 --- a/src/oidcop/oidc/registration.py +++ b/src/oidcop/oidc/registration.py @@ -3,7 +3,6 @@ import json import logging import secrets -import time from typing import List from urllib.parse import urlencode from urllib.parse import urlparse @@ -86,31 +85,33 @@ def verify_url(url: str, urlset: List[list]) -> bool: def secret(seed: str, sid: str): - msg = "{}{}{}".format(time.time(), secrets.token_urlsafe(16), sid).encode("utf-8") + msg = "{}{}{}".format(utc_time_sans_frac(), secrets.token_urlsafe(16), sid).encode("utf-8") csum = hmac.new(as_bytes(seed), msg, hashlib.sha224) return csum.hexdigest() def comb_uri(args): - for param in ["redirect_uris", "post_logout_redirect_uris"]: - if param not in args: - continue - + redirect_uris = args.get("redirect_uris") + if redirect_uris: val = [] - for base, query_dict in args[param]: + for base, query_dict in redirect_uris: if query_dict: - query_string = urlencode( - [ - (key, v) - for key in query_dict - for v in query_dict[key] - ] - ) - val.append("{base}?{query_string}") + query_string = urlencode([(key, v) for key in query_dict for v in query_dict[key]]) + val.append(f"{base}?{query_string}") else: val.append(base) - args[param] = val + args["redirect_uris"] = val + + post_logout_redirect_uri = args.get("post_logout_redirect_uri") + if post_logout_redirect_uri: + base, query_dict = post_logout_redirect_uri + if query_dict: + query_string = urlencode([(key, v) for key in query_dict for v in query_dict[key]]) + val = f"{base}?{query_string}" + else: + val = base + args["post_logout_redirect_uri"] = val request_uris = args.get("request_uris") if request_uris: @@ -179,17 +180,15 @@ def do_client_registration(self, request, client_id, ignore=None): if key not in ignore: _cinfo[key] = val - if "post_logout_redirect_uris" in request: - plruri = [] - for uri in request["post_logout_redirect_uris"]: - if urlparse(uri).fragment: - err = self.error_cls( - error="invalid_configuration_parameter", - error_description="post_logout_redirect_uris contains fragment", - ) - return err - plruri.append(split_uri(uri)) - _cinfo["post_logout_redirect_uris"] = plruri + _uri = request.get("post_logout_redirect_uri") + if _uri: + if urlparse(_uri).fragment: + err = self.error_cls( + error="invalid_configuration_parameter", + error_description="post_logout_redirect_uri contains fragment", + ) + return err + _cinfo["post_logout_redirect_uri"] = split_uri(_uri) if "redirect_uris" in request: try: @@ -217,9 +216,10 @@ def do_client_registration(self, request, client_id, ignore=None): if "sector_identifier_uri" in request: try: - (_cinfo["si_redirects"], _cinfo["sector_id"],) = self._verify_sector_identifier( - request - ) + ( + _cinfo["si_redirects"], + _cinfo["sector_id"], + ) = self._verify_sector_identifier(request) except InvalidSectorIdentifier as err: return ResponseMessage( error="invalid_configuration_parameter", error_description=str(err) @@ -292,7 +292,9 @@ def verify_redirect_uris(registration_request): pass else: logger.error( - "InvalidRedirectURI: scheme:%s, hostname:%s", p.scheme, p.hostname, + "InvalidRedirectURI: scheme:%s, hostname:%s", + p.scheme, + p.hostname, ) raise InvalidRedirectURIError( "Redirect_uri must use custom " "scheme or http and localhost" @@ -384,17 +386,21 @@ def client_registration_setup(self, request, new_id=True, set_secret=True): try: request.verify() except (MessageException, ValueError) as err: - logger.error("request.verify() on %s", request) - return ResponseMessage( - error="invalid_configuration_request", error_description="%s" % err - ) + logger.error("request.verify() error on %s", request) + _error = "invalid_configuration_request" + if len(err.args) > 1: + if err.args[1] == "initiate_login_uri": + _error = "invalid_client_metadata" + + return ResponseMessage(error=_error, error_description="%s" % err) request.rm_blanks() try: self.match_client_request(request) except CapabilitiesMisMatch as err: return ResponseMessage( - error="invalid_request", error_description="Don't support proposed %s" % err, + error="invalid_request", + error_description="Don't support proposed %s" % err, ) _context = self.server_get("endpoint_context") @@ -429,7 +435,9 @@ def client_registration_setup(self, request, new_id=True, set_secret=True): _context.cdb[client_id] = _cinfo _cinfo = self.do_client_registration( - request, client_id, ignore=["redirect_uris", "policy_uri", "logo_uri", "tos_uri"], + request, + client_id, + ignore=["redirect_uris", "policy_uri", "logo_uri", "tos_uri"], ) if isinstance(_cinfo, ResponseMessage): return _cinfo @@ -470,7 +478,17 @@ def process_request(self, request=None, new_id=True, set_secret=True, **kwargs): else: _context = self.server_get("endpoint_context") _cookie = _context.new_cookie( - name=_context.cookie_handler.name["register"], client_id=reg_resp["client_id"], + name=_context.cookie_handler.name["register"], + client_id=reg_resp["client_id"], ) return {"response_args": reg_resp, "cookie": _cookie, "response_code": 201} + + def process_verify_error(self, exception): + _error = "invalid_request" + if isinstance(exception, ValueError): + if len(exception.args) > 1: + if exception.args[1] == "initiate_login_uri": + _error = "invalid_client_metadata" + + return self.error_cls(error=_error, error_description=f"{exception}") diff --git a/src/oidcop/oidc/session.py b/src/oidcop/oidc/session.py index 4e35141e..79a7f9dc 100644 --- a/src/oidcop/oidc/session.py +++ b/src/oidcop/oidc/session.py @@ -51,6 +51,8 @@ def do_front_channel_logout_iframe(cinfo, iss, sid): except KeyError: flsr = False + logger.debug(f"frontchannel_logout_uri: {frontchannel_logout_uri}") + logger.debug(f"frontchannel_logout_session_required: {flsr}") if flsr: _query = {"iss": iss, "sid": sid} if "?" in frontchannel_logout_uri: @@ -61,6 +63,7 @@ def do_front_channel_logout_iframe(cinfo, iss, sid): _np = p._replace(query="") frontchannel_logout_uri = _np.geturl() + logger.debug(f"IFrame query: {_query}") _iframe = '