diff --git a/.gitignore b/.gitignore index fea22345..831dbd0f 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,5 @@ venv.bak/ src/oidcendpoint.egg-info/ .iframes/ +tests/pairwise.salt +tests/public.salt diff --git a/backburner/device_authorization.py b/backburner/device_authorization.py deleted file mode 100644 index 4bd3fc02..00000000 --- a/backburner/device_authorization.py +++ /dev/null @@ -1,65 +0,0 @@ -from oidcmsg.oauth2.device_authorization import AuthorizationRequest -from oidcmsg.oauth2.device_authorization import AuthorizationResponse -from oidcmsg.time_util import utc_time_sans_frac - -from oidcop import rndstr -from oidcop.endpoint import Endpoint - - -class DeviceAuthorizationEndpoint(Endpoint): - request_cls = AuthorizationRequest - response_cls = AuthorizationResponse - request_format = "urlencoded" - response_format = "json" - response_placement = "body" - endpoint_name = "device_authorization_endpoint" - name = "device_authorization" - - def __init__(self, server_get, **kwargs): - Endpoint.__init__(self, server_get, **kwargs) - self.verification_uri = kwargs.get("verification_uri") - self.expires_in = kwargs.get("expires_in", 300) - self.interval = kwargs.get("interval", 5) - - _context = server_get("endpoint_context") - _context.dev_auth_db = {} - - def process_request(self, request=None, **kwargs): - """ - Produces a device code and an end-user - code and provides the end-user verification URI. - - :param request: - :param kwargs: - :return: - """ - _device_code = rndstr(32) - _user_code = rndstr(8) - - _response = { - "device_code": _device_code, - "user_code": _user_code, - "verification_uri": self.verification_uri, - "expires_in": self.expires_in, - "interval": self.interval, - } - _info = { - "device_code": _device_code, - "user_code": _user_code, - "exp": utc_time_sans_frac() + self.expires_in, - "interval": self.interval, - } - - self.server_get("endpoint_context").dev_auth_db.set(_user_code, _info) - return {"response_args": _response} - - def verification_endpoint(self, query): - """ - Where the device's pull query is handled. - - :param query: - :return: - """ - _response = {} - - return _response diff --git a/doc/source/contents/conf.rst b/doc/source/contents/conf.rst index 6eafffee..47680d4c 100644 --- a/doc/source/contents/conf.rst +++ b/doc/source/contents/conf.rst @@ -8,6 +8,38 @@ issuer The issuer ID of the OP, a unique value in URI format. +---- +seed +---- + +Used in dynamic client registration endpoint when creating a new client_secret. +If unset it will be random. + +-------- +password +-------- + +Encryption key used to encrypt the SessionID (sid) in access_token. +If unset it will be random. + +---- +salt +---- + +Salt, value or filename, used in sub_funcs (pairwise, public) for creating the opaque hash of *sub* claim. + +----------- +session_key +----------- + +An example:: + + "session_key": { + "filename": "private/session_jwk.json", + "type": "OCT", + "use": "sig" + }, + ------ add_on ------ @@ -47,7 +79,7 @@ An example:: "authentication": { "user": { - "acr": "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD", + "acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword", "class": "oidcop.user_authn.user.UserPassJinja2", "kwargs": { "verify_endpoint": "verify/user", @@ -195,8 +227,11 @@ An example:: "class": "oidcop.oauth2.introspection.Introspection", "kwargs": { "client_authn_method": [ - "client_secret_post" - ], + "client_secret_post", + "client_secret_basic", + "client_secret_jwt", + "private_key_jwt" + ] "release": [ "username" ] @@ -266,6 +301,15 @@ An example:: } } +You can specify which algoritms are supported, for example in userinfo_endpoint:: + + "userinfo_signing_alg_values_supported": OIDC_SIGN_ALGS, + "userinfo_encryption_alg_values_supported": OIDC_ENC_ALGS, + +Or in authorization endpoint:: + + "request_object_encryption_alg_values_supported": OIDC_ENC_ALGS, + ------------ httpc_params ------------ @@ -307,10 +351,16 @@ An example:: "uri_path": "static/jwks.json" }, +*read_only* means that on each restart the keys will created and overwritten with new ones. +This can be useful during the first time the project have been executed, then to keep them as they are *read_only* would be configured to *True*. + --------------- login_hint2acrs --------------- +OIDC Login hint support, it's optional. +It matches the login_hint paramenter to one or more Authentication Contexts. + An example:: "login_hint2acrs": { @@ -318,12 +368,23 @@ An example:: "kwargs": { "scheme_map": { "email": [ - "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD" + "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword" ] } } }, +oidc-op supports the following authn contexts: + +- UNSPECIFIED, urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified +- INTERNETPROTOCOLPASSWORD, urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword +- MOBILETWOFACTORCONTRACT, urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract +- PASSWORDPROTECTEDTRANSPORT, urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport +- PASSWORD, urn:oasis:names:tc:SAML:2.0:ac:classes:Password +- TLSCLIENT, urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient +- TIMESYNCTOKEN, urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken + + ----- authz ----- @@ -358,31 +419,46 @@ An example:: } }, - ------------ -session_key ------------ - -An example:: - - "session_key": { - "filename": "private/session_jwk.json", - "type": "OCT", - "use": "sig" - }, - ------------ template_dir ------------ +The HTML Template directory used by Jinja2, used by endpoint context + template loader, as:: + + Environment(loader=FileSystemLoader(template_dir), autoescape=True) + An example:: "template_dir": "templates" +For any further customization of template here an example of what used in django-oidc-op + + "authentication": { + "user": { + "acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword", + "class": "oidc_provider.users.UserPassDjango", + "kwargs": { + "verify_endpoint": "verify/oidc_user_login/", + "template": "oidc_login.html", + + "page_header": "Testing log in", + "submit_btn": "Get me in!", + "user_label": "Nickname", + "passwd_label": "Secret sauce" + } + } + }, + ------------------ token_handler_args ------------------ +Token handler is an intermediate interface used by and endpoint to manage + the tokens' default behaviour, like lifetime and minting policies. + With it we can create a token that's linked to another, and keep relations between many tokens + in session and grants management. + An example:: "token_handler_args": { @@ -442,6 +518,44 @@ An example:: } } +jwks_defs can be replaced eventually by `jwks_file`:: + + "jwks_file": f"{OIDC_JWKS_PRIVATE_PATH}/token_jwks.json", + +You can even select wich algorithms to support in id_token, eg:: + + "id_token": { + "class": "oidcop.token.id_token.IDToken", + "kwargs": { + "id_token_signing_alg_values_supported": [ + "RS256", + "RS512", + "ES256", + "ES512", + "PS256", + "PS512", + ], + "id_token_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "A192KW", + "A256KW", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW", + ], + "id_token_encryption_enc_values_supported": [ + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + 'A128GCM', + 'A192GCM', + 'A256GCM' + ], + } + } + -------- userinfo -------- @@ -456,8 +570,8 @@ An example:: } This is somethig that can be customized. -For example in a django project we would use something like -the following (see `example/django_op/oidc_provider`):: +For example in the django-oidc-op implementation is used something like +the following:: "userinfo": { "class": "oidc_provider.users.UserInfo", diff --git a/doc/source/contents/session_management.rst b/doc/source/contents/session_management.rst index 22227c4b..26e99371 100644 --- a/doc/source/contents/session_management.rst +++ b/doc/source/contents/session_management.rst @@ -372,16 +372,16 @@ max_usage used :::: -How many times the token has been used + How many times the token has been used based_on :::::::: -Reference to the token that was used to mint this token. Might be empty if the -token was minted based on the grant it belongs to. + Reference to the token that was used to mint this token. Might be empty if the + token was minted based on the grant it belongs to. id :: -Token identifier + Token identifier Session Info API ---------------- @@ -391,38 +391,56 @@ add_subordinate +++++++++++++++ .. _`add_subordinate`: + ... + remove_subordinate ++++++++++++++++++ .. _`removed_subordinate`: + ... + revoke ++++++ .. _`revoke`: + ... + is_revoked ++++++++++ .. _`is_revoked`: + ... + to_json +++++++ .. _`to_json`: + ... + from_json +++++++++ .. _`from_json`: + ... + Grant API --------- .. _`Grant API`: + ... + Token API --------- .. _`Token API`: + ... + Session Manager API ------------------- .. _`Session Manager API`: + ... + create_session ++++++++++++++ .. _create_session: @@ -463,77 +481,77 @@ add_grant +++++++++ .. _add_grant: -add_grant(self, user_id, client_id, **kwargs) + add_grant(self, user_id, client_id, **kwargs) find_token ++++++++++ .. _find_token: -find_token(self, session_id, token_value) + find_token(self, session_id, token_value) get_authentication_event ++++++++++++++++++++++++ .. _get_authentication_event: -get_authentication_event(self, session_id) + get_authentication_event(self, session_id) get_client_session_info +++++++++++++++++++++++ .. _get_client_session_info: -get_client_session_info(self, session_id) + get_client_session_info(self, session_id) get_grant_by_response_type ++++++++++++++++++++++++++ .. _get_grant_by_response_type: -get_grant_by_response_type(self, user_id, client_id) + get_grant_by_response_type(self, user_id, client_id) get_session_info ++++++++++++++++ .. _get_session_info: -get_session_info(self, session_id) + get_session_info(self, session_id) get_session_info_by_token +++++++++++++++++++++++++ .. _get_session_info_by_token: -get_session_info_by_token(self, token_value) + get_session_info_by_token(self, token_value) get_sids_by_user_id +++++++++++++++++++ .. _get_sids_by_user_id: -get_sids_by_user_id(self, user_id) + get_sids_by_user_id(self, user_id) get_user_info +++++++++++++ .. _get_user_info: -get_user_info(self, uid) + get_user_info(self, uid) grants ++++++ .. _grants: -grants(self, session_id) + grants(self, session_id) revoke_client_session +++++++++++++++++++++ .. _revoke_client_session: -revoke_client_session(self, session_id) + revoke_client_session(self, session_id) revoke_grant ++++++++++++ .. _revoke_grant: -revoke_grant(self, session_id) + revoke_grant(self, session_id) revoke_token ++++++++++++ .. _revoke_token: -revoke_token(self, session_id, token_value, recursive=False) + revoke_token(self, session_id, token_value, recursive=False) diff --git a/doc/source/contents/usage.md b/doc/source/contents/usage.md index fff1bf67..6c51e765 100644 --- a/doc/source/contents/usage.md +++ b/doc/source/contents/usage.md @@ -47,3 +47,46 @@ The identity representation with the information fetched from the user info endp ![Logout](../_images/4.png) We can even test the single logout + + +Introspection endpoint +---------------------- + +Here an example about how to consume oidc-op introspection endpoint. +This example uses a client with an HTTP Basic Authentication:: + + import base64 + import requests + + TOKEN = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzY29wZSI6IFsib3BlbmlkIiwgInByb2ZpbGUiLCAiZW1haWwiLCAiYWRkcmVzcyIsICJwaG9uZSJdLCAiYXVkIjogWyJvTHlSajdzSkozWHZBWWplRENlOHJRIl0sICJqdGkiOiAiOWQzMjkzYjZiYmNjMTFlYmEzMmU5ODU0MWIwNzE1ZWQiLCAiY2xpZW50X2lkIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic3ViIjogIm9MeVJqN3NKSjNYdkFZamVEQ2U4clEiLCAic2lkIjogIlowRkJRVUZCUW1keGJIVlpkRVJKYkZaUFkxQldaa0pQVUVGc1pHOUtWWFZ3VFdkZmVEY3diMVprYmpSamRrNXRMVzB4YTNnelExOHlRbHBHYTNRNVRHZEdUUzF1UW1sMlkzVnhjRE5sUm01dFRFSmxabGRXYVhJeFpFdHVSV2xtUzBKcExWTmFaRzV3VjJodU0yNXlSbTU0U1ZWVWRrWTRRM2x2UWs1TlpVUk9SazlGVlVsRWRteGhjWGx2UWxWRFdubG9WbTFvZGpORlVUSnBkaTFaUTFCcFptZFRabWRDVWt0YVNuaGtOalZCWVhkcGJFNXpaV2xOTTFCMk0yaE1jMDV0ZGxsUlRFc3dObWxsYUcxa1lrTkhkemhuU25OaWFWZE1kVUZzZDBwWFdWbzFiRWhEZFhGTFFXWTBPVzl5VjJOUk4zaGtPRDA9IiwgInR0eXBlIjogIlQiLCAiaXNzIjogImh0dHBzOi8vMTI3LjAuMC4xOjgwMDAiLCAiaWF0IjogMTYyMTc3NzMwNSwgImV4cCI6IDE2MjE3ODA5MDV9.pVqxUNznsoZu9ND18IEMJIHDOT6_HxzoFiTLsniNdbAdXTuOoiaKeRTqtDyjT9WuUPszdHkVjt5xxeFX8gQMuA" + + data = { + 'token': TOKEN, + 'token_type_hint': 'access_token' + } + + _basic_secret = base64.b64encode( + f'{"oLyRj7sJJ3XvAYjeDCe8rQ"}:{"53fb49f2a6501ec775355c89750dc416744a3253138d5a04e409b313"}'.encode() + ) + headers = { + 'Authorization': f"Basic {_basic_secret.decode()}" + } + + requests.post('https://127.0.0.1:8000/introspection', verify=False, data=data, headers=headers) + + +oidc-op will return a json response like this:: + + { + "active": true, + "scope": "openid profile email address phone", + "client_id": "oLyRj7sJJ3XvAYjeDCe8rQ", + "token_type": "access_token", + "exp": 0, + "iat": 1621777305, + "sub": "a7b0dea2958aec275a789d7d7dc8e7d09c6316dd4fc6ae92742ed3297e14dded", + "iss": "https://127.0.0.1:8000", + "aud": [ + "oLyRj7sJJ3XvAYjeDCe8rQ" + ] + } diff --git a/example/django_op/.gitignore b/example/django_op/.gitignore deleted file mode 100644 index 302d5602..00000000 --- a/example/django_op/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*/db.sqlite3 -example/db.sqlite3 -example/data/static/* -!example/data/static/README.md -__pycache__/* -*.pyc - diff --git a/example/django_op/AUTHORS b/example/django_op/AUTHORS deleted file mode 100644 index b995f227..00000000 --- a/example/django_op/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -Giuseppe De Marco diff --git a/example/django_op/LICENSE b/example/django_op/LICENSE deleted file mode 100644 index fe2e6d0d..00000000 --- a/example/django_op/LICENSE +++ /dev/null @@ -1,190 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - Copyright 2019 Giuseppe De Marco - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/example/django_op/README.md b/example/django_op/README.md index a03cea12..108e9bd2 100644 --- a/example/django_op/README.md +++ b/example/django_op/README.md @@ -1,150 +1,3 @@ # django-oidc-op -A Django implementation of an **OIDC Provider** on top of [jwtconnect.io](https://jwtconnect.io/). -To configure a standard OIDC Provider you have to edit the oidcop configuration file. -See `example/example/oidc_op.conf.yaml` to get in. -This project is based on [Roland Hedberg's oidc-op](https://github.com/rohe/oidc-op). -Oidcendpoint supports the following standards and drafts: - -- [OpenID Connect Core 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-core-1_0.html) -- [OpenID Connect Discovery 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-discovery-1_0.html) -- [OpenID Connect Dynamic Client Registration 1.0 incorporating errata set 1](https://openid.net/specs/openid-connect-registration-1_0.html) -- [OpenID Connect Session Management 1.0](https://openid.net/specs/openid-connect-session-1_0.html) -- [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html) -- [OAuth2 Token introspection](https://tools.ietf.org/html/rfc7662) - -It also supports the followings `add_ons` modules. - -- Custom scopes, that extends [OIDC standard ScopeClaims](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) -- PKCE, [Proof Key for Code Exchange by OAuth Public Clients](https://tools.ietf.org/html/rfc7636) - -## Status - -The development status of this project is *experimental*, something goes wrong following latest oidcendpoint releases. -A roadmap for a stable release is still in progress. - - -Works: - -- Relying-Parties Admin UI completed, unit tests included (works v1.0.1) -- Session and SSO management completed (works v1.0.1) -- Logout session handler - -Work in progress: -- KeyJAR and default storage (issuer, keybundles) (TODO with a full storage handler integration) -- Cookie handling, at this time we do not use cookies (disabled in configuration) -- Custom scopes - -## Run the example demo - -```` -git clone https://github.com/peppelinux/django-oidc-op.git -cd django-oidc-op - -pip install -r requirements.txt - -cd example -pip install -r requirements.txt -./manage.py migrate -./manage.py createsuperuser -./manage.py collectstatic - -## debug server -gunicorn example.wsgi -b0.0.0.0:8000 --keyfile=./data/oidc_op/certs/key.pem --certfile=./data/oidc_op/certs/cert.pem --reload --timeout 3600 --capture-output --enable-stdio-inheritance -```` - -You can use [JWTConnect-Python-OidcRP](https://github.com/openid/JWTConnect-Python-OidcRP) as follow: -``` -cd JWTConnect-Python-OidcRP -RP_LOGFILE_NAME="./flrp.django.log" python3 -m flask_rp.wsgi ../django-oidc-op/example/data/oidc_rp/conf.django.yaml -``` - -You can also use a scripted RP handler on top of oidc-rp -```` -python3 snippets/rp_handler.py -c example/data/oidc_rp/conf.django.yaml -u myuser -p mypass -iss django_oidc_op -```` - - -## Configure OIDC endpoint - -#### Django settings.py parameters - -`OIDCENDPOINT_CONFIG`: The path containing the oidc-op configuration file. -`OIDC_OP_AUTHN_SALT_SIZE`: Salt size in byte, default: 4 (Integer). - -#### JWKs -These following files needed to be present in `data/oidc_op/private` otherwise they will be created on the first time automatically. - -1. session.json (JWK symmetric); - -These are not used anymore, disabled in op conf.yaml: -1. cookie_sign_jwk.json (JWK symmetric); -2. cookie_enc_jwk.json (JWK symmetric), optional, see `conf.yaml`. - -To create them by hands comment out `'read_only': False'` in `conf.yaml`, -otherwise they will be created automatically on each run. - -A JWK creation example would be: -```` -jwkgen --kty SYM > data/oidc_op/private/cookie_enc_jwk.json -```` - -## Django specific implementation - -This project rely interely on behaviour and features provided by oidcendpoint, to get an exaustive integration in Django it -adopt the following customizations. - -#### DataStore management -Oidcendpoint have some data persistence: -You can use oidcendpoint's standard `oidcmsg.storage.abfile.AbstractFileSystem` or Django models (Work in Progress). - -#### UserInfo endpoint - -Claims to be released are configured in `op.server_info.user_info` (in `conf.yaml`). -The attributes release and user authentication mechanism rely on classes implemented in `oidc_op.users.py`. - -Configuration Example: - -```` - userinfo: - class: oidc_op.users.UserInfo - kwargs: - # map claims to django user attributes here: - claims_map: - phone_number: telephone - family_name: last_name - given_name: first_name - email: email - verified_email: email -```` - -#### Relying-Party search panel - -See `oidc_op.models` and `oidc_op.admin`, an UI was built to configure new RP via Django admin backend. -![Alt text](images/rp_search.png) - -#### Relying-Party Registration -![Alt text](images/rp.png) - -#### Session management and token preview -![Alt text](images/oidc_session2.png) - -## OIDC endpoint url prefix -Can be configured in `urls.py` and also in oidc_op `conf.yaml`. - -- /oidc/endpoint/ - - -## Running tests - -running tests -```` -./manage.py test --pdb oidc_op.tests.01_client_db -```` - -## code coverage -```` -coverage erase -coverage run manage.py test -coverage report -```` +The Django oidc-op implementation is available here [django-oidc-op github page](https://github.com/peppelinux/django-oidc-op/tree/develop). diff --git a/example/django_op/example/.coveragerc b/example/django_op/example/.coveragerc deleted file mode 100644 index 7fe59bb4..00000000 --- a/example/django_op/example/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -source = . - -[report] -fail_under = 100 -show_missing = True -skip_covered = True - - diff --git a/example/django_op/example/accounts/__init__.py b/example/django_op/example/accounts/__init__.py deleted file mode 100644 index b48c1ec8..00000000 --- a/example/django_op/example/accounts/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -default_app_config = 'accounts.apps.AccountsConfig' - diff --git a/example/django_op/example/accounts/admin.py b/example/django_op/example/accounts/admin.py deleted file mode 100644 index 08ca4e8d..00000000 --- a/example/django_op/example/accounts/admin.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.contrib import admin -from django.utils.translation import ugettext, ugettext_lazy as _ -from django.contrib.auth.admin import UserAdmin - -from .models import User, PersistentId -from .admin_inlines import PersistentIdInline - - -@admin.register(User) -class CustomUserAdmin(UserAdmin): - readonly_fields = ('date_joined', 'last_login',) - list_display = ('username', 'email', 'is_active', - 'is_staff', 'is_superuser', ) - list_editable = ('is_active', 'is_staff', 'is_superuser',) - inlines = [PersistentIdInline,] - fieldsets = ( - (None, {'fields': (('username', 'is_active', 'is_staff', 'is_superuser', ), - ('password'), - ('origin'), - ) - }), - (_('Anagrafica'), {'fields': (('first_name', 'last_name'), - 'email', - ('taxpayer_id',), - ('gender', - 'place_of_birth', 'birth_date',), - ) - }), - - (_('Permissions'), {'fields': ('groups', 'user_permissions'), - 'classes':('collapse',) - }), - - - (_('Date accessi sistema'), {'fields': (('date_joined', - 'last_login', )) - }), - ) - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('username', 'password1', 'password2'), - }), - ) - -admin.site.unregister(User) -admin.site.register(User, CustomUserAdmin) - - -@admin.register(PersistentId) -class PersistentIdAdmin(admin.ModelAdmin): - list_display = ('user', 'persistent_id', 'recipient_id', 'created') - list_filter = ('created',) diff --git a/example/django_op/example/accounts/admin_inlines.py b/example/django_op/example/accounts/admin_inlines.py deleted file mode 100644 index 33b0047c..00000000 --- a/example/django_op/example/accounts/admin_inlines.py +++ /dev/null @@ -1,8 +0,0 @@ -from django import forms -from django.contrib import admin - -from .models import * - -class PersistentIdInline(admin.TabularInline): - model = PersistentId - extra = 0 diff --git a/example/django_op/example/accounts/apps.py b/example/django_op/example/accounts/apps.py deleted file mode 100644 index e4b019ea..00000000 --- a/example/django_op/example/accounts/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class AccountsConfig(AppConfig): - name = 'accounts' - verbose_name = "Autenticazione e Autorizzazione Utenti" diff --git a/example/django_op/example/accounts/forms.py b/example/django_op/example/accounts/forms.py deleted file mode 100644 index 95ec770d..00000000 --- a/example/django_op/example/accounts/forms.py +++ /dev/null @@ -1,2 +0,0 @@ -from django import forms -from .models import * diff --git a/example/django_op/example/accounts/migrations/0001_initial.py b/example/django_op/example/accounts/migrations/0001_initial.py deleted file mode 100644 index 6e41e8b7..00000000 --- a/example/django_op/example/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 2.2.5 on 2019-09-21 16:47 - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0011_update_proxy_permissions'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('first_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Name')), - ('last_name', models.CharField(blank=True, max_length=30, null=True, verbose_name='Surname')), - ('is_active', models.BooleanField(default=True, verbose_name='active')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='email address')), - ('taxpayer_id', models.CharField(blank=True, max_length=32, null=True, verbose_name="Taxpayer's identification number")), - ('gender', models.CharField(blank=True, choices=[('male', 'Maschio'), ('female', 'Femmina'), ('other', 'Altro')], max_length=12, null=True, verbose_name='Genere')), - ('place_of_birth', models.CharField(blank=True, choices=[('Aruba', 'Aruba'), ('Afghanistan', 'Afghanistan'), ('Angola', 'Angola'), ('Anguilla', 'Anguilla'), ('Åland Islands', 'Åland Islands'), ('Albania', 'Albania'), ('Andorra', 'Andorra'), ('United Arab Emirates', 'United Arab Emirates'), ('Argentina', 'Argentina'), ('Armenia', 'Armenia'), ('American Samoa', 'American Samoa'), ('Antarctica', 'Antarctica'), ('French Southern Territories', 'French Southern Territories'), ('Antigua and Barbuda', 'Antigua and Barbuda'), ('Australia', 'Australia'), ('Austria', 'Austria'), ('Azerbaijan', 'Azerbaijan'), ('Burundi', 'Burundi'), ('Belgium', 'Belgium'), ('Benin', 'Benin'), ('Bonaire, Sint Eustatius and Saba', 'Bonaire, Sint Eustatius and Saba'), ('Burkina Faso', 'Burkina Faso'), ('Bangladesh', 'Bangladesh'), ('Bulgaria', 'Bulgaria'), ('Bahrain', 'Bahrain'), ('Bahamas', 'Bahamas'), ('Bosnia and Herzegovina', 'Bosnia and Herzegovina'), ('Saint Barthélemy', 'Saint Barthélemy'), ('Belarus', 'Belarus'), ('Belize', 'Belize'), ('Bermuda', 'Bermuda'), ('Bolivia, Plurinational State of', 'Bolivia, Plurinational State of'), ('Brazil', 'Brazil'), ('Barbados', 'Barbados'), ('Brunei Darussalam', 'Brunei Darussalam'), ('Bhutan', 'Bhutan'), ('Bouvet Island', 'Bouvet Island'), ('Botswana', 'Botswana'), ('Central African Republic', 'Central African Republic'), ('Canada', 'Canada'), ('Cocos (Keeling) Islands', 'Cocos (Keeling) Islands'), ('Switzerland', 'Switzerland'), ('Chile', 'Chile'), ('China', 'China'), ("Côte d'Ivoire", "Côte d'Ivoire"), ('Cameroon', 'Cameroon'), ('Congo, The Democratic Republic of the', 'Congo, The Democratic Republic of the'), ('Congo', 'Congo'), ('Cook Islands', 'Cook Islands'), ('Colombia', 'Colombia'), ('Comoros', 'Comoros'), ('Cabo Verde', 'Cabo Verde'), ('Costa Rica', 'Costa Rica'), ('Cuba', 'Cuba'), ('Curaçao', 'Curaçao'), ('Christmas Island', 'Christmas Island'), ('Cayman Islands', 'Cayman Islands'), ('Cyprus', 'Cyprus'), ('Czechia', 'Czechia'), ('Germany', 'Germany'), ('Djibouti', 'Djibouti'), ('Dominica', 'Dominica'), ('Denmark', 'Denmark'), ('Dominican Republic', 'Dominican Republic'), ('Algeria', 'Algeria'), ('Ecuador', 'Ecuador'), ('Egypt', 'Egypt'), ('Eritrea', 'Eritrea'), ('Western Sahara', 'Western Sahara'), ('Spain', 'Spain'), ('Estonia', 'Estonia'), ('Ethiopia', 'Ethiopia'), ('Finland', 'Finland'), ('Fiji', 'Fiji'), ('Falkland Islands (Malvinas)', 'Falkland Islands (Malvinas)'), ('France', 'France'), ('Faroe Islands', 'Faroe Islands'), ('Micronesia, Federated States of', 'Micronesia, Federated States of'), ('Gabon', 'Gabon'), ('United Kingdom', 'United Kingdom'), ('Georgia', 'Georgia'), ('Guernsey', 'Guernsey'), ('Ghana', 'Ghana'), ('Gibraltar', 'Gibraltar'), ('Guinea', 'Guinea'), ('Guadeloupe', 'Guadeloupe'), ('Gambia', 'Gambia'), ('Guinea-Bissau', 'Guinea-Bissau'), ('Equatorial Guinea', 'Equatorial Guinea'), ('Greece', 'Greece'), ('Grenada', 'Grenada'), ('Greenland', 'Greenland'), ('Guatemala', 'Guatemala'), ('French Guiana', 'French Guiana'), ('Guam', 'Guam'), ('Guyana', 'Guyana'), ('Hong Kong', 'Hong Kong'), ('Heard Island and McDonald Islands', 'Heard Island and McDonald Islands'), ('Honduras', 'Honduras'), ('Croatia', 'Croatia'), ('Haiti', 'Haiti'), ('Hungary', 'Hungary'), ('Indonesia', 'Indonesia'), ('Isle of Man', 'Isle of Man'), ('India', 'India'), ('British Indian Ocean Territory', 'British Indian Ocean Territory'), ('Ireland', 'Ireland'), ('Iran, Islamic Republic of', 'Iran, Islamic Republic of'), ('Iraq', 'Iraq'), ('Iceland', 'Iceland'), ('Israel', 'Israel'), ('Italy', 'Italy'), ('Jamaica', 'Jamaica'), ('Jersey', 'Jersey'), ('Jordan', 'Jordan'), ('Japan', 'Japan'), ('Kazakhstan', 'Kazakhstan'), ('Kenya', 'Kenya'), ('Kyrgyzstan', 'Kyrgyzstan'), ('Cambodia', 'Cambodia'), ('Kiribati', 'Kiribati'), ('Saint Kitts and Nevis', 'Saint Kitts and Nevis'), ('Korea, Republic of', 'Korea, Republic of'), ('Kuwait', 'Kuwait'), ("Lao People's Democratic Republic", "Lao People's Democratic Republic"), ('Lebanon', 'Lebanon'), ('Liberia', 'Liberia'), ('Libya', 'Libya'), ('Saint Lucia', 'Saint Lucia'), ('Liechtenstein', 'Liechtenstein'), ('Sri Lanka', 'Sri Lanka'), ('Lesotho', 'Lesotho'), ('Lithuania', 'Lithuania'), ('Luxembourg', 'Luxembourg'), ('Latvia', 'Latvia'), ('Macao', 'Macao'), ('Saint Martin (French part)', 'Saint Martin (French part)'), ('Morocco', 'Morocco'), ('Monaco', 'Monaco'), ('Moldova, Republic of', 'Moldova, Republic of'), ('Madagascar', 'Madagascar'), ('Maldives', 'Maldives'), ('Mexico', 'Mexico'), ('Marshall Islands', 'Marshall Islands'), ('North Macedonia', 'North Macedonia'), ('Mali', 'Mali'), ('Malta', 'Malta'), ('Myanmar', 'Myanmar'), ('Montenegro', 'Montenegro'), ('Mongolia', 'Mongolia'), ('Northern Mariana Islands', 'Northern Mariana Islands'), ('Mozambique', 'Mozambique'), ('Mauritania', 'Mauritania'), ('Montserrat', 'Montserrat'), ('Martinique', 'Martinique'), ('Mauritius', 'Mauritius'), ('Malawi', 'Malawi'), ('Malaysia', 'Malaysia'), ('Mayotte', 'Mayotte'), ('Namibia', 'Namibia'), ('New Caledonia', 'New Caledonia'), ('Niger', 'Niger'), ('Norfolk Island', 'Norfolk Island'), ('Nigeria', 'Nigeria'), ('Nicaragua', 'Nicaragua'), ('Niue', 'Niue'), ('Netherlands', 'Netherlands'), ('Norway', 'Norway'), ('Nepal', 'Nepal'), ('Nauru', 'Nauru'), ('New Zealand', 'New Zealand'), ('Oman', 'Oman'), ('Pakistan', 'Pakistan'), ('Panama', 'Panama'), ('Pitcairn', 'Pitcairn'), ('Peru', 'Peru'), ('Philippines', 'Philippines'), ('Palau', 'Palau'), ('Papua New Guinea', 'Papua New Guinea'), ('Poland', 'Poland'), ('Puerto Rico', 'Puerto Rico'), ("Korea, Democratic People's Republic of", "Korea, Democratic People's Republic of"), ('Portugal', 'Portugal'), ('Paraguay', 'Paraguay'), ('Palestine, State of', 'Palestine, State of'), ('French Polynesia', 'French Polynesia'), ('Qatar', 'Qatar'), ('Réunion', 'Réunion'), ('Romania', 'Romania'), ('Russian Federation', 'Russian Federation'), ('Rwanda', 'Rwanda'), ('Saudi Arabia', 'Saudi Arabia'), ('Sudan', 'Sudan'), ('Senegal', 'Senegal'), ('Singapore', 'Singapore'), ('South Georgia and the South Sandwich Islands', 'South Georgia and the South Sandwich Islands'), ('Saint Helena, Ascension and Tristan da Cunha', 'Saint Helena, Ascension and Tristan da Cunha'), ('Svalbard and Jan Mayen', 'Svalbard and Jan Mayen'), ('Solomon Islands', 'Solomon Islands'), ('Sierra Leone', 'Sierra Leone'), ('El Salvador', 'El Salvador'), ('San Marino', 'San Marino'), ('Somalia', 'Somalia'), ('Saint Pierre and Miquelon', 'Saint Pierre and Miquelon'), ('Serbia', 'Serbia'), ('South Sudan', 'South Sudan'), ('Sao Tome and Principe', 'Sao Tome and Principe'), ('Suriname', 'Suriname'), ('Slovakia', 'Slovakia'), ('Slovenia', 'Slovenia'), ('Sweden', 'Sweden'), ('Eswatini', 'Eswatini'), ('Sint Maarten (Dutch part)', 'Sint Maarten (Dutch part)'), ('Seychelles', 'Seychelles'), ('Syrian Arab Republic', 'Syrian Arab Republic'), ('Turks and Caicos Islands', 'Turks and Caicos Islands'), ('Chad', 'Chad'), ('Togo', 'Togo'), ('Thailand', 'Thailand'), ('Tajikistan', 'Tajikistan'), ('Tokelau', 'Tokelau'), ('Turkmenistan', 'Turkmenistan'), ('Timor-Leste', 'Timor-Leste'), ('Tonga', 'Tonga'), ('Trinidad and Tobago', 'Trinidad and Tobago'), ('Tunisia', 'Tunisia'), ('Turkey', 'Turkey'), ('Tuvalu', 'Tuvalu'), ('Taiwan, Province of China', 'Taiwan, Province of China'), ('Tanzania, United Republic of', 'Tanzania, United Republic of'), ('Uganda', 'Uganda'), ('Ukraine', 'Ukraine'), ('United States Minor Outlying Islands', 'United States Minor Outlying Islands'), ('Uruguay', 'Uruguay'), ('United States', 'United States'), ('Uzbekistan', 'Uzbekistan'), ('Holy See (Vatican City State)', 'Holy See (Vatican City State)'), ('Saint Vincent and the Grenadines', 'Saint Vincent and the Grenadines'), ('Venezuela, Bolivarian Republic of', 'Venezuela, Bolivarian Republic of'), ('Virgin Islands, British', 'Virgin Islands, British'), ('Virgin Islands, U.S.', 'Virgin Islands, U.S.'), ('Viet Nam', 'Viet Nam'), ('Vanuatu', 'Vanuatu'), ('Wallis and Futuna', 'Wallis and Futuna'), ('Samoa', 'Samoa'), ('Yemen', 'Yemen'), ('South Africa', 'South Africa'), ('Zambia', 'Zambia'), ('Zimbabwe', 'Zimbabwe')], max_length=30, null=True, verbose_name='Luogo di nascita')), - ('birth_date', models.DateField(blank=True, null=True, verbose_name='Data di nascita')), - ('origin', models.CharField(blank=True, max_length=254, null=True, verbose_name='from which conenctor this user come from')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name_plural': 'Users', - 'ordering': ['username'], - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='PersistentId', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('persistent_id', models.CharField(blank=True, max_length=254, null=True, verbose_name='SAML Persistent Stored ID')), - ('recipient_id', models.CharField(blank=True, max_length=254, null=True, verbose_name='SAML ServiceProvider entityID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Persistent Id', - 'verbose_name_plural': 'Persistent Id', - }, - ), - ] diff --git a/example/django_op/example/accounts/migrations/0002_auto_20191202_1526.py b/example/django_op/example/accounts/migrations/0002_auto_20191202_1526.py deleted file mode 100644 index 57e6923c..00000000 --- a/example/django_op/example/accounts/migrations/0002_auto_20191202_1526.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0 on 2019-12-02 15:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='user', - name='place_of_birth', - field=models.CharField(blank=True, choices=[('Aruba', 'Aruba'), ('Afghanistan', 'Afghanistan'), ('Angola', 'Angola'), ('Anguilla', 'Anguilla'), ('Åland Islands', 'Åland Islands'), ('Albania', 'Albania'), ('Andorra', 'Andorra'), ('United Arab Emirates', 'United Arab Emirates'), ('Argentina', 'Argentina'), ('Armenia', 'Armenia'), ('American Samoa', 'American Samoa'), ('Antarctica', 'Antarctica'), ('French Southern Territories', 'French Southern Territories'), ('Antigua and Barbuda', 'Antigua and Barbuda'), ('Australia', 'Australia'), ('Austria', 'Austria'), ('Azerbaijan', 'Azerbaijan'), ('Burundi', 'Burundi'), ('Belgium', 'Belgium'), ('Benin', 'Benin'), ('Bonaire, Sint Eustatius and Saba', 'Bonaire, Sint Eustatius and Saba'), ('Burkina Faso', 'Burkina Faso'), ('Bangladesh', 'Bangladesh'), ('Bulgaria', 'Bulgaria'), ('Bahrain', 'Bahrain'), ('Bahamas', 'Bahamas'), ('Bosnia and Herzegovina', 'Bosnia and Herzegovina'), ('Saint Barthélemy', 'Saint Barthélemy'), ('Belarus', 'Belarus'), ('Belize', 'Belize'), ('Bermuda', 'Bermuda'), ('Bolivia, Plurinational State of', 'Bolivia, Plurinational State of'), ('Brazil', 'Brazil'), ('Barbados', 'Barbados'), ('Brunei Darussalam', 'Brunei Darussalam'), ('Bhutan', 'Bhutan'), ('Bouvet Island', 'Bouvet Island'), ('Botswana', 'Botswana'), ('Central African Republic', 'Central African Republic'), ('Canada', 'Canada'), ('Cocos (Keeling) Islands', 'Cocos (Keeling) Islands'), ('Switzerland', 'Switzerland'), ('Chile', 'Chile'), ('China', 'China'), ("Côte d'Ivoire", "Côte d'Ivoire"), ('Cameroon', 'Cameroon'), ('Congo, The Democratic Republic of the', 'Congo, The Democratic Republic of the'), ('Congo', 'Congo'), ('Cook Islands', 'Cook Islands'), ('Colombia', 'Colombia'), ('Comoros', 'Comoros'), ('Cabo Verde', 'Cabo Verde'), ('Costa Rica', 'Costa Rica'), ('Cuba', 'Cuba'), ('Curaçao', 'Curaçao'), ('Christmas Island', 'Christmas Island'), ('Cayman Islands', 'Cayman Islands'), ('Cyprus', 'Cyprus'), ('Czechia', 'Czechia'), ('Germany', 'Germany'), ('Djibouti', 'Djibouti'), ('Dominica', 'Dominica'), ('Denmark', 'Denmark'), ('Dominican Republic', 'Dominican Republic'), ('Algeria', 'Algeria'), ('Ecuador', 'Ecuador'), ('Egypt', 'Egypt'), ('Eritrea', 'Eritrea'), ('Western Sahara', 'Western Sahara'), ('Spain', 'Spain'), ('Estonia', 'Estonia'), ('Ethiopia', 'Ethiopia'), ('Finland', 'Finland'), ('Fiji', 'Fiji'), ('Falkland Islands (Malvinas)', 'Falkland Islands (Malvinas)'), ('France', 'France'), ('Faroe Islands', 'Faroe Islands'), ('Micronesia, Federated States of', 'Micronesia, Federated States of'), ('Gabon', 'Gabon'), ('United Kingdom', 'United Kingdom'), ('Georgia', 'Georgia'), ('Guernsey', 'Guernsey'), ('Ghana', 'Ghana'), ('Gibraltar', 'Gibraltar'), ('Guinea', 'Guinea'), ('Guadeloupe', 'Guadeloupe'), ('Gambia', 'Gambia'), ('Guinea-Bissau', 'Guinea-Bissau'), ('Equatorial Guinea', 'Equatorial Guinea'), ('Greece', 'Greece'), ('Grenada', 'Grenada'), ('Greenland', 'Greenland'), ('Guatemala', 'Guatemala'), ('French Guiana', 'French Guiana'), ('Guam', 'Guam'), ('Guyana', 'Guyana'), ('Hong Kong', 'Hong Kong'), ('Heard Island and McDonald Islands', 'Heard Island and McDonald Islands'), ('Honduras', 'Honduras'), ('Croatia', 'Croatia'), ('Haiti', 'Haiti'), ('Hungary', 'Hungary'), ('Indonesia', 'Indonesia'), ('Isle of Man', 'Isle of Man'), ('India', 'India'), ('British Indian Ocean Territory', 'British Indian Ocean Territory'), ('Ireland', 'Ireland'), ('Iran, Islamic Republic of', 'Iran, Islamic Republic of'), ('Iraq', 'Iraq'), ('Iceland', 'Iceland'), ('Israel', 'Israel'), ('Italy', 'Italy'), ('Jamaica', 'Jamaica'), ('Jersey', 'Jersey'), ('Jordan', 'Jordan'), ('Japan', 'Japan'), ('Kazakhstan', 'Kazakhstan'), ('Kenya', 'Kenya'), ('Kyrgyzstan', 'Kyrgyzstan'), ('Cambodia', 'Cambodia'), ('Kiribati', 'Kiribati'), ('Saint Kitts and Nevis', 'Saint Kitts and Nevis'), ('Korea, Republic of', 'Korea, Republic of'), ('Kuwait', 'Kuwait'), ("Lao People's Democratic Republic", "Lao People's Democratic Republic"), ('Lebanon', 'Lebanon'), ('Liberia', 'Liberia'), ('Libya', 'Libya'), ('Saint Lucia', 'Saint Lucia'), ('Liechtenstein', 'Liechtenstein'), ('Sri Lanka', 'Sri Lanka'), ('Lesotho', 'Lesotho'), ('Lithuania', 'Lithuania'), ('Luxembourg', 'Luxembourg'), ('Latvia', 'Latvia'), ('Macao', 'Macao'), ('Saint Martin (French part)', 'Saint Martin (French part)'), ('Morocco', 'Morocco'), ('Monaco', 'Monaco'), ('Moldova, Republic of', 'Moldova, Republic of'), ('Madagascar', 'Madagascar'), ('Maldives', 'Maldives'), ('Mexico', 'Mexico'), ('Marshall Islands', 'Marshall Islands'), ('North Macedonia', 'North Macedonia'), ('Mali', 'Mali'), ('Malta', 'Malta'), ('Myanmar', 'Myanmar'), ('Montenegro', 'Montenegro'), ('Mongolia', 'Mongolia'), ('Northern Mariana Islands', 'Northern Mariana Islands'), ('Mozambique', 'Mozambique'), ('Mauritania', 'Mauritania'), ('Montserrat', 'Montserrat'), ('Martinique', 'Martinique'), ('Mauritius', 'Mauritius'), ('Malawi', 'Malawi'), ('Malaysia', 'Malaysia'), ('Mayotte', 'Mayotte'), ('Namibia', 'Namibia'), ('New Caledonia', 'New Caledonia'), ('Niger', 'Niger'), ('Norfolk Island', 'Norfolk Island'), ('Nigeria', 'Nigeria'), ('Nicaragua', 'Nicaragua'), ('Niue', 'Niue'), ('Netherlands', 'Netherlands'), ('Norway', 'Norway'), ('Nepal', 'Nepal'), ('Nauru', 'Nauru'), ('New Zealand', 'New Zealand'), ('Oman', 'Oman'), ('Pakistan', 'Pakistan'), ('Panama', 'Panama'), ('Pitcairn', 'Pitcairn'), ('Peru', 'Peru'), ('Philippines', 'Philippines'), ('Palau', 'Palau'), ('Papua New Guinea', 'Papua New Guinea'), ('Poland', 'Poland'), ('Puerto Rico', 'Puerto Rico'), ("Korea, Democratic People's Republic of", "Korea, Democratic People's Republic of"), ('Portugal', 'Portugal'), ('Paraguay', 'Paraguay'), ('Palestine, State of', 'Palestine, State of'), ('French Polynesia', 'French Polynesia'), ('Qatar', 'Qatar'), ('Réunion', 'Réunion'), ('Romania', 'Romania'), ('Russian Federation', 'Russian Federation'), ('Rwanda', 'Rwanda'), ('Saudi Arabia', 'Saudi Arabia'), ('Sudan', 'Sudan'), ('Senegal', 'Senegal'), ('Singapore', 'Singapore'), ('South Georgia and the South Sandwich Islands', 'South Georgia and the South Sandwich Islands'), ('Saint Helena, Ascension and Tristan da Cunha', 'Saint Helena, Ascension and Tristan da Cunha'), ('Svalbard and Jan Mayen', 'Svalbard and Jan Mayen'), ('Solomon Islands', 'Solomon Islands'), ('Sierra Leone', 'Sierra Leone'), ('El Salvador', 'El Salvador'), ('San Marino', 'San Marino'), ('Somalia', 'Somalia'), ('Saint Pierre and Miquelon', 'Saint Pierre and Miquelon'), ('Serbia', 'Serbia'), ('South Sudan', 'South Sudan'), ('Sao Tome and Principe', 'Sao Tome and Principe'), ('Suriname', 'Suriname'), ('Slovakia', 'Slovakia'), ('Slovenia', 'Slovenia'), ('Sweden', 'Sweden'), ('Eswatini', 'Eswatini'), ('Sint Maarten (Dutch part)', 'Sint Maarten (Dutch part)'), ('Seychelles', 'Seychelles'), ('Syrian Arab Republic', 'Syrian Arab Republic'), ('Turks and Caicos Islands', 'Turks and Caicos Islands'), ('Chad', 'Chad'), ('Togo', 'Togo'), ('Thailand', 'Thailand'), ('Tajikistan', 'Tajikistan'), ('Tokelau', 'Tokelau'), ('Turkmenistan', 'Turkmenistan'), ('Timor-Leste', 'Timor-Leste'), ('Tonga', 'Tonga'), ('Trinidad and Tobago', 'Trinidad and Tobago'), ('Tunisia', 'Tunisia'), ('Turkey', 'Turkey'), ('Tuvalu', 'Tuvalu'), ('Taiwan, Province of China', 'Taiwan, Province of China'), ('Tanzania, United Republic of', 'Tanzania, United Republic of'), ('Uganda', 'Uganda'), ('Ukraine', 'Ukraine'), ('United States Minor Outlying Islands', 'United States Minor Outlying Islands'), ('Uruguay', 'Uruguay'), ('United States', 'United States'), ('Uzbekistan', 'Uzbekistan'), ('Holy See (Vatican City State)', 'Holy See (Vatican City State)'), ('Saint Vincent and the Grenadines', 'Saint Vincent and the Grenadines'), ('Venezuela, Bolivarian Republic of', 'Venezuela, Bolivarian Republic of'), ('Virgin Islands, British', 'Virgin Islands, British'), ('Virgin Islands, U.S.', 'Virgin Islands, U.S.'), ('Viet Nam', 'Viet Nam'), ('Vanuatu', 'Vanuatu'), ('Wallis and Futuna', 'Wallis and Futuna'), ('Samoa', 'Samoa'), ('Yemen', 'Yemen'), ('South Africa', 'South Africa'), ('Zambia', 'Zambia'), ('Zimbabwe', 'Zimbabwe')], max_length=56, null=True, verbose_name='Luogo di nascita'), - ), - ] diff --git a/example/django_op/example/accounts/migrations/__init__.py b/example/django_op/example/accounts/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/django_op/example/accounts/models.py b/example/django_op/example/accounts/models.py deleted file mode 100644 index bc994780..00000000 --- a/example/django_op/example/accounts/models.py +++ /dev/null @@ -1,82 +0,0 @@ -import pycountry - -from django.db import models -from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import AbstractUser -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from django.conf import settings - - -class User(AbstractUser): - GENDER= ( - ( 'male', _('Maschio')), - ( 'female', _('Femmina')), - ( 'other', _('Altro')), - ) - - first_name = models.CharField(_('Name'), max_length=30, - blank=True, null=True) - last_name = models.CharField(_('Surname'), max_length=30, - blank=True, null=True) - is_active = models.BooleanField(_('active'), default=True) - email = models.EmailField('email address', blank=True, null=True) - taxpayer_id = models.CharField(_('Taxpayer\'s identification number'), - max_length=32, - blank=True, null=True) - gender = models.CharField(_('Genere'), choices=GENDER, - max_length=12, blank=True, null=True) - place_of_birth = models.CharField('Luogo di nascita', max_length=56, - blank=True, null=True, - choices=[(i.name, i.name) for i in pycountry.countries]) - birth_date = models.DateField('Data di nascita', - null=True, blank=True) - origin = models.CharField(_('from which conenctor this user come from'), - max_length=254, - blank=True, null=True) - - class Meta: - ordering = ['username'] - verbose_name_plural = _("Users") - - @property - def uid(self): - return self.username.split('@')[0] - - def persistent_id(self, entityid): - """ returns persistent id related to a recipient (sp) entity id - """ - pid = PersistentId.objects.filter(user=self, - recipient_id=entityid).last() - if pid: - return pid.persistent_id - - def get_oidc_birthdate(self): - return self.birth_date.strftime('%Y-%m-%d') if self.birth_date else '' - - def get_oidc_lastlogin(self): - return self.last_login.timestamp() if self.last_login else '' - - def __str__(self): - return '{} {}'.format(self.first_name, self.last_name) - - -class PersistentId(models.Model): - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - persistent_id = models.CharField(_('SAML Persistent Stored ID'), - max_length=254, - blank=True, null=True) - recipient_id = models.CharField(_('SAML ServiceProvider entityID'), - max_length=254, - blank=True, null=True) - created = models.DateTimeField(auto_now_add=True) - - class Meta: - verbose_name = _('Persistent Id') - verbose_name_plural = _('Persistent Id') - - def __str__(self): - return '{}: {} to {} [{}]'.format(self.user, - self.persistent_id, - self.recipient_id, - self.created) diff --git a/example/django_op/example/accounts/templatetags/__init__.py b/example/django_op/example/accounts/templatetags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/django_op/example/accounts/templatetags/has_group.py b/example/django_op/example/accounts/templatetags/has_group.py deleted file mode 100644 index aa402dab..00000000 --- a/example/django_op/example/accounts/templatetags/has_group.py +++ /dev/null @@ -1,9 +0,0 @@ -from django import template -from django.contrib.auth.models import Group - -register = template.Library() - -@register.filter(name='has_group') -def has_group(user, group_name): - group = Group.objects.get(name=group_name) - return group in user.groups.all() diff --git a/example/django_op/example/accounts/tests.py b/example/django_op/example/accounts/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/example/django_op/example/accounts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/example/django_op/example/accounts/urls.py b/example/django_op/example/accounts/urls.py deleted file mode 100644 index 7bd77a54..00000000 --- a/example/django_op/example/accounts/urls.py +++ /dev/null @@ -1,26 +0,0 @@ -"""betaCRM URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.10/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.urls import path -from .views import * - -app_name="accounts" - -urlpatterns = [ - - # url(r'^login/$', Login, name='login'), - # path('logout', Logout, name='logout'), - -] diff --git a/example/django_op/example/accounts/views.py b/example/django_op/example/accounts/views.py deleted file mode 100644 index 3050ccaf..00000000 --- a/example/django_op/example/accounts/views.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseNotFound - -from django.shortcuts import render -from django.contrib.auth.decorators import login_required -from django.shortcuts import get_object_or_404 -from .models import * -from .forms import * - -from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ValidationError - -from django.template import RequestContext -from django.core.urlresolvers import reverse - -from django.contrib.auth import authenticate, login, logout -# from dal import autocomplete -# -# class UserAutocomplete(autocomplete.Select2QuerySetView): - # def get_queryset(self): - # if not self.request.user.is_authenticated(): - # return User.objects.none() - # qs = User.objects.all() - # if self.q: - # qs = qs.filter( - # username__icontains=self.q - # ) - # return qs diff --git a/example/django_op/example/data/oidc_op/certs/cert.pem b/example/django_op/example/data/oidc_op/certs/cert.pem deleted file mode 100644 index 210fd240..00000000 --- a/example/django_op/example/data/oidc_op/certs/cert.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFUDCCAzigAwIBAgIJAJWgBcizyJrFMA0GCSqGSIb3DQEBCwUAMD0xCzAJBgNV -BAYTAlNFMQ0wCwYDVQQKDARPSURGMR8wHQYDVQQDDBZGZWQgYXdhcmUgUlAgdGVz -dCB0b29sMB4XDTE3MDIxMzE5MDg1MloXDTE4MDIxMzE5MDg1MlowPTELMAkGA1UE -BhMCU0UxDTALBgNVBAoMBE9JREYxHzAdBgNVBAMMFkZlZCBhd2FyZSBSUCB0ZXN0 -IHRvb2wwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC3NrEL+VKs00NT -R+ZpGRxvDoeLhD7EM+uf7IqHl6IN3H6pflAOE8YqnTepdglhGH4a7nyftINTZjDU -86anR+OKPoY2Padf4E+YceJOcaT6lB5XOWxBu4j3wDRHb6jMUwMDUXHsmh389Bvx -X44KSYe/mhjkrIV8bolhT9NpNjPVUdUvpwpSxDOhSjq7BCmfdvXJrNNYElEQaDSc -yJ4h6BAOp/FfdnWKAeiVDpIF5QqZgr0gzKiV5LEvwsNfHynsLgrlgK2+Fd8qIqbC -/fHtB1BEL3h01dlBR1Y4ocMM5we23Phe4lwQs8QojPTnnr14fWynrjNi0Km0TcMT -TDHVnw5qO5dSr4LpBcfIo82YWpj6lTEKQwKin+SPz0k0kD4E83rtsGp8n3FWHVAo -BsIJ4O58REi3YTh1NCe/bjsQWiFOPW0N9GOl0UTOUj90cGVbO9i91aDFHHQWOIiA -VsmZ35yOjQ031It9Kzv4YcmWXQcdKYnzUQ5eSXZPmJFoebKgQF6neFlg1hp6uDKi -NRxkaPWGVCVZXPmgRwVcFdbxI8OpNqPEFQGskUPGJS5CF1o8o6wuVwPSSwxDVoYM -12TTdATH1he4cK69ej/1F2oHCVQ0KE46fNABaxNKxGls0bPPPJBPrQBjoAR2qxgg -iFz2DjumVC3EySwXLsH4tXTjyuVbSQIDAQABo1MwUTAdBgNVHQ4EFgQUiD0bTabj -Q0Pf0vVJneGr5TQRO+4wHwYDVR0jBBgwFoAUiD0bTabjQ0Pf0vVJneGr5TQRO+4w -DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAnYCE5MdqVXHBxMGZ -1bIZxwLg9pe5poaX7l7XGdXxnBKWxfqwCx2UHQZIBdV3eIt8lgtuOL1en9ZCHAIY -X0OZCafQ1Jzx3nXV4qOoolfmri0DQs60LPozoXKW61mah8fFhf/XdjuZxYH+XVV6 -39E08MY4ZWDzzNoDe5zhGWw+IOfowx5wNTtZ8CipWUv4FiO9cUZJ/1hnJgE0CQNH -v4v0g0lIuWs7eArbzvxTu3jHWx/+eYvl2TSYxEHpVulbesnI27M34nS0OePqbywO -eGBtM65UuCCBh27FO+O7qJWA3sRPuw/cll0vi69WVYHO5rk7yji1hiTT2MKTEizP -GmdT/FXG4nEsM6WaEe4FMJN6cZf49BUzRcEdW6k8i2YIysHf8fi3Xv1JF74OB5bF -TogV/Fu/LzXsfA/XTj9ki0hUNmueyNT/xBD5tOH4FqHQvMWpjpzfwI90ENVeY+Ad -BCU2Ck1HBEuUhUNaC1d6QkU6pn3voPvaWK49+T9NyrFVMNHVWHeLUHJ/i9kgWXLl -TgAbTCmnJOHTxxCVCf40EjOpPR3hlCadYr8vOGyuHPk1M2Lppgh2kQtFX5ubhhfW -IKP5TPKuZlu3z9RjfUvIxqWC6cbwjlOGIx2K0uCnIbpTzTuaLHJSWWRUpDzNL6lg -V620B7/n1jo2JDudjhjD2uLekJg= ------END CERTIFICATE----- diff --git a/example/django_op/example/data/oidc_op/certs/key.pem b/example/django_op/example/data/oidc_op/certs/key.pem deleted file mode 100644 index 15449664..00000000 --- a/example/django_op/example/data/oidc_op/certs/key.pem +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC3NrEL+VKs00NT -R+ZpGRxvDoeLhD7EM+uf7IqHl6IN3H6pflAOE8YqnTepdglhGH4a7nyftINTZjDU -86anR+OKPoY2Padf4E+YceJOcaT6lB5XOWxBu4j3wDRHb6jMUwMDUXHsmh389Bvx -X44KSYe/mhjkrIV8bolhT9NpNjPVUdUvpwpSxDOhSjq7BCmfdvXJrNNYElEQaDSc -yJ4h6BAOp/FfdnWKAeiVDpIF5QqZgr0gzKiV5LEvwsNfHynsLgrlgK2+Fd8qIqbC -/fHtB1BEL3h01dlBR1Y4ocMM5we23Phe4lwQs8QojPTnnr14fWynrjNi0Km0TcMT -TDHVnw5qO5dSr4LpBcfIo82YWpj6lTEKQwKin+SPz0k0kD4E83rtsGp8n3FWHVAo -BsIJ4O58REi3YTh1NCe/bjsQWiFOPW0N9GOl0UTOUj90cGVbO9i91aDFHHQWOIiA -VsmZ35yOjQ031It9Kzv4YcmWXQcdKYnzUQ5eSXZPmJFoebKgQF6neFlg1hp6uDKi -NRxkaPWGVCVZXPmgRwVcFdbxI8OpNqPEFQGskUPGJS5CF1o8o6wuVwPSSwxDVoYM -12TTdATH1he4cK69ej/1F2oHCVQ0KE46fNABaxNKxGls0bPPPJBPrQBjoAR2qxgg -iFz2DjumVC3EySwXLsH4tXTjyuVbSQIDAQABAoICAQCoZ801hGdKFKa91kkkMcDB -FEnjJBvNnSvoRDTRjb+XniWPBlvvlJ2CbiDL04OrjCfd+Xj0E6ji7/vSwmNdP+cX -G4GiOemvZy/CoGu0TyGmcp+w7Udk5Exx7moff7NYnLUYR7TAFqmZ6YgFxh95tTzi -EXLwPuQ0DCabHBTnkLr0SdP7iT8j9NTAXMq/PIRF38LtLb7WJX/95Mr3kjBIWlbo -IdbsOKaxxC9VU59Fa9LiaBoQHA6aOSvlCtEqjiqqvWemrTEGmHQY9uDyOxo1FZPi -GQBP5IFeT4Qhag8vvOyKWXKzRL37XEHiRC6Y+ICQUDmfp6/0FHjpEtFM26yy/xDv -ZtL7/b7TEQMmp2CWD8WV8a9oalTRqyrGTBeeSg6CV5tnx3wnM0krkCvJ+Eadki23 -Wp34s7v8NPmVMTqG/UIW21tmzb40KjXNI8MgNXASBIKm9W2z2xXQ07xELsSfWm9O -p0umh1xHLqX7rNmigg/odW3K9aocF8NOhuc4aYgVZH18sMhkhja3dgwCe8YSImyW -0uHZC6wKIXnD44lS2BmdYsIY/k+uZKNum6lE7x/F1V2vbzkzShuJ7VCD3IhQW6nK -XNQBXju/CnMiMW6mpZsSZG8mIjx8hNKLYv492ZNgnbeP2HHM5WAsKTOKLO0FldFS -sbRSXTTM40j7AcurS7DKQQKCAQEA2WdkRhGXOuOlHSq/W4YZ6Mq2kydp46ARQS8b -zKbUXX6+7GyU6TSB71eblP4003NGx8rdasyZTpexRH4sTKv0/GLM2eSDEi7/GV1w -HISwdIa8NlHiOT9qPONqdhH0KDy5lDrCTMa9B5QpbYo4l/F/4O52zJc1CuRacpyi -58hY3Me2UND2yHqb2TKxOwwHumE8FEMs9CqixLE4oAaoiNdJi08pyg7o/6oxPaUE -CKmGX6r9eW5piFCLGAkmfAgBjYejrFDAp8eY6Yx5dRWMdLddQnm/5tl0rzFho+71 -UwtOIZtowKeWms1N/+duOmcfYyDsRJ/Ec3pzxphzcHrWEllP9wKCAQEA171qkSxv -+53viIJbaJ636emDg4kZ3asGLODefEcbe0XS0xHmsb+WZpRIBkNMJFj3k2IYcUSO -7DObemF4ln9CJY+DxHZJzr/mo8T3X0yt0aK8O75+fXHQ/991kUMcx21BmXMjybYj -TA5vv956AYV9Kt37ye87dYMtEINtchdukYqyrLZ9+0lBV1XrGKALMC68EyyTtDFs -AtJzKVTYnKNkYFWkA6cq+GZvlEbx74dZopH/yVo+P/wGiU5AH1bq5847uq5LIwIU -j2ZkKBJr8Y3YvFjAaRNRGNXOhHUo3BPkgkYZGnC2WP9UJT3w7PgjwyUpbFZurwIr -Sz1QdbNZ+spevwKCAQBgyN6jMwGYfe/r5DP8kt7F/Dj7mfhSFdiYpFhD66FvXhWx -O0Wv7GhMHTxuQB1UZWWFXJLmEN/PVUjdrS4blBIkqfd4qXqQhcubhzV5/Lhxp+ny -ZNHJmqm5IaUrmyKPJzmW+/G0LGXLEfK/iWFYg3LiuEa7HjXG+5IopAMCHPcyktZf -dCfpaGwpbZ/pIZnvJ4qPmrhQmwqLdjo3Q7+T7AQZuMxp3+lqqGHzh5scIBxqSr09 -aiIhRXom4Sv427eVQmVjOTALgZhZoOgRb95vt5IVHg6IvxZrSBin2qHsroPCAmXI -HtO1ZuDqpCU2auJWRznn8xiKMGGKcCQ0VvsmgAxRAoIBAQDPsB7OQRxQ+3skTHIZ -Jmrg+ZdM4oiPGFyqiZRFyeKP6ukJnvsadNkiSW+I7/J2L1uve8kSCbEZfJkZ2InR -QBN6u01brZBiQ+WSFUUbbmMLJIHXdgypUQ+ltAanYBdteSWkxu5V+kzCpEc6S7/i -hRK5WNhTT0ZLW4vfkNak9h/QZtiZYlmntp77p8/adgAvU15liw1qdAWKNfT9fhvF -t5ojD28EwUKhvWN/OEkikYdd9PVsbr7ss//K4RTj1rXvkF952N6mhhMq9aRH22wl -L6vNrhcVUK5KnVHhvDQoodHjA/6YsJcq2Cq2a4nrZvpum/DjxdVqD0mEdjNmC9H8 -mCNbAoIBAHbkApjatORw6Bb+zAbfLs2vKLMs0sVABmA2AzTukm8+k3Clji4npGxh -IGj4c2kBa93yOd25qONoNvFfcig+LbCnq5aT8qSLTl7iecRNvvAlxA1r7MHRqjYO -bFGAM5cCZC+hpOmXF80IOmQMfaV33tCHJ0uf1fOvkreAQxPOJqEskYGFHqN8zfeW -zsSMnea+oHvfAhHmQcikJV/YiomYb0Urz838o5o+JLTkBs+miwPNTZW5iVEnYLUh -NtABZU3c1ohXAw8i4Z/Jdmxzsro75D3ekRfa/coPCcnUK0MqYd8C/uEVe5rgXOWZ -Svp9rK9sO9LqfKBeV9NKW9/wb/X6lU4= ------END PRIVATE KEY----- diff --git a/example/django_op/example/data/oidc_rp/conf.django.yaml b/example/django_op/example/data/oidc_rp/conf.django.yaml deleted file mode 100644 index 699a6f89..00000000 --- a/example/django_op/example/data/oidc_rp/conf.django.yaml +++ /dev/null @@ -1,159 +0,0 @@ -logging: - version: 1 - disable_existing_loggers: False - root: - handlers: - - default - - console - level: DEBUG - loggers: - 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: 8099 -base_url: "https://127.0.0.1:8099" - -# If BASE is https these has to be specified -webserver: - port: '{port}' - server_cert: "certs/cert.pem" - server_key: "certs/key.pem" - domain: '{domain}' - -# This is just for testing an local usage. In all other cases it MUST be True -httpc_params: - verify: False - -key_defs: &keydef - - - "type": "RSA" - "key": '' - "use": ["sig"] - - - "type": "EC" - "crv": "P-256" - "use": ["sig"] - -# html_home: 'html' -# secret_key: 'secret_key' -# session_cookie_name: 'rp_session' -# preferred_url_scheme: 'https' - -rp_keys: - 'private_path': './private/jwks.json' - 'key_defs': *keydef - 'public_path': './static/jwks.json' - # this will create the jwks files if they absent - 'read_only': False - -# information used when registering the client, this may be the same for all OPs -client_preferences: &prefs - application_name: rp_test - application_type: web - contacts: - - ops@example.com - response_types: - - code - scope: - - openid - - that_scope - - profile - - email - - address - - phone - token_endpoint_auth_method: - - client_secret_basic - - client_secret_post - -services: &services - discovery: &disc - class: oidcservice.oidc.provider_info_discovery.ProviderInfoDiscovery - kwargs: {} - registration: ®ist - class: oidcservice.oidc.registration.Registration - kwargs: {} - authorization: &authz - class: oidcservice.oidc.authorization.Authorization - kwargs: {} - accesstoken: &acctok - class: oidcservice.oidc.access_token.AccessToken - kwargs: {} - userinfo: &userinfo - class: oidcservice.oidc.userinfo.UserInfo - kwargs: {} - end_session: &sess - class: oidcservice.oidc.end_session.EndSession - kwargs: {} - -clients: - # The ones that support webfinger, OP discovery and client registration - # This is the default, any client that is not listed here is expected to - # support dynamic discovery and registration. - "": - client_preferences: *prefs - redirect_uris: None - services: *services - - django_oidc_op: - client_preferences: *prefs - - # if you create a client through ADMIN UI ... - #client_id: 1UUl6cwNigmj - #client_secret: 78be88872d5877c4ddb209335f4eb2fc5118a481a195a454c8b2ebcb - # this redirect_uri must be statically configured in op's rp cdb! - redirect_uris: - - https://127.0.0.1:8099/authz_cb/django_oidc_op - issuer: https://127.0.0.1:8000/ - jwks_uri: https://127.0.0.1:8099/static/jwks.json - - services: - discovery: *disc - registration: *regist - authorization: *authz - accesstoken: *acctok - userinfo: *userinfo - end_session: *sess - add_ons: - pkce: - function: oidcservice.oidc.add_on.pkce.add_pkce_support - kwargs: - code_challenge_length: 64 - code_challenge_method: S256 - - shib_oidc_op: - client_preferences: *prefs - - # if you create a client through ADMIN UI ... - client_id: demo_rp - client_secret: topsecret2020_______ - # this redirect_uri must be statically configured in op's rp cdb! - redirect_uris: - - https://127.0.0.1:8099/authz_cb/shib_oidc_op - issuer: https://idp.testunical.it/ - jwks_uri: https://127.0.0.1:8099/static/jwks.json - - services: - discovery: *disc - registration: *regist - authorization: *authz - accesstoken: *acctok - userinfo: *userinfo - end_session: *sess - add_ons: - pkce: - function: oidcservice.oidc.add_on.pkce.add_pkce_support - kwargs: - code_challenge_length: 64 - code_challenge_method: S256 diff --git a/example/django_op/example/data/oidc_rp/conf.json b/example/django_op/example/data/oidc_rp/conf.json deleted file mode 100644 index e12541e2..00000000 --- a/example/django_op/example/data/oidc_rp/conf.json +++ /dev/null @@ -1,322 +0,0 @@ -{ - "logging": { - "version": 1, - "disable_existing_loggers": false, - "root": { - "handlers": [ - "console", - "file" - ], - "level": "DEBUG" - }, - "loggers": { - "idp": { - "level": "DEBUG" - } - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "stream": "ext://sys.stdout", - "formatter": "default" - }, - "file": { - "class": "logging.FileHandler", - "filename": "debug.log", - "formatter": "default" - } - }, - "formatters": { - "default": { - "format": "%(asctime)s %(name)s %(levelname)s %(message)s" - } - } - }, - "port": 8090, - "domain": "127.0.0.1", - "base_url": "https://{domain}:{port}", - "httpc_params": { - "verify": false - }, - "keydefs": [ - { - "type": "RSA", - "key": "", - "use": [ - "sig" - ] - }, - { - "type": "EC", - "crv": "P-256", - "use": [ - "sig" - ] - } - ], - "rp_keys": { - "private_path": "private/jwks.json", - "key_defs": [ - { - "type": "RSA", - "key": "", - "use": [ - "sig" - ] - }, - { - "type": "EC", - "crv": "P-256", - "use": [ - "sig" - ] - } - ], - "public_path": "static/jwks.json", - "read_only": false - }, - "client_preferences": { - "application_name": "rphandler", - "application_type": "web", - "contacts": [ - "ops@example.com" - ], - "response_types": [ - "code" - ], - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ], - "token_endpoint_auth_method": [ - "client_secret_basic", - "client_secret_post" - ] - }, - "services": { - "discovery": { - "class": "oidcrp.oidc.provider_info_discovery.ProviderInfoDiscovery", - "kwargs": {} - }, - "registration": { - "class": "oidcrp.oidc.registration.Registration", - "kwargs": {} - }, - "authorization": { - "class": "oidcrp.oidc.authorization.Authorization", - "kwargs": {} - }, - "accesstoken": { - "class": "oidcrp.oidc.access_token.AccessToken", - "kwargs": {} - }, - "userinfo": { - "class": "oidcrp.oidc.userinfo.UserInfo", - "kwargs": {} - }, - "end_session": { - "class": "oidcrp.oidc.end_session.EndSession", - "kwargs": {} - } - }, - "clients": { - "": { - "client_preferences": { - "application_name": "rphandler", - "application_type": "web", - "contacts": [ - "ops@example.com" - ], - "response_types": [ - "code" - ], - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ], - "token_endpoint_auth_method": [ - "client_secret_basic", - "client_secret_post" - ] - }, - "redirect_uris": "None", - "services": { - "discovery": { - "class": "oidcrp.oidc.provider_info_discovery.ProviderInfoDiscovery", - "kwargs": {} - }, - "registration": { - "class": "oidcrp.oidc.registration.Registration", - "kwargs": {} - }, - "authorization": { - "class": "oidcrp.oidc.authorization.Authorization", - "kwargs": {} - }, - "accesstoken": { - "class": "oidcrp.oidc.access_token.AccessToken", - "kwargs": {} - }, - "userinfo": { - "class": "oidcrp.oidc.userinfo.UserInfo", - "kwargs": {} - }, - "end_session": { - "class": "oidcrp.oidc.end_session.EndSession", - "kwargs": {} - } - } - }, - "flask_provider": { - "client_preferences": { - "application_name": "rphandler", - "application_type": "web", - "contacts": [ - "ops@example.com" - ], - "response_types": [ - "code" - ], - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ], - "token_endpoint_auth_method": [ - "client_secret_basic", - "client_secret_post" - ] - }, - "issuer": "https://127.0.0.1:5000/", - "redirect_uris": [ - "https://{domain}:{port}/authz_cb/local" - ], - "post_logout_redirect_uris": [ - "https://{domain}:{port}/session_logout/local" - ], - "frontchannel_logout_uri": "https://{domain}:{port}/fc_logout/local", - "frontchannel_logout_session_required": true, - "backchannel_logout_uri": "https://{domain}:{port}/bc_logout/local", - "backchannel_logout_session_required": true, - "services": { - "discovery": { - "class": "oidcrp.oidc.provider_info_discovery.ProviderInfoDiscovery", - "kwargs": {} - }, - "registration": { - "class": "oidcrp.oidc.registration.Registration", - "kwargs": {} - }, - "authorization": { - "class": "oidcrp.oidc.authorization.Authorization", - "kwargs": {} - }, - "accesstoken": { - "class": "oidcrp.oidc.access_token.AccessToken", - "kwargs": {} - }, - "userinfo": { - "class": "oidcrp.oidc.userinfo.UserInfo", - "kwargs": {} - }, - "end_session": { - "class": "oidcrp.oidc.end_session.EndSession", - "kwargs": {} - } - }, - "add_ons": { - "pkce": { - "function": "oidcrp.oauth2.add_on.pkce.add_support", - "kwargs": { - "code_challenge_length": 64, - "code_challenge_method": "S256" - } - } - } - }, - "django_provider": { - "client_preferences": { - "application_name": "rphandler", - "application_type": "web", - "contacts": [ - "ops@example.com" - ], - "response_types": [ - "code" - ], - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ], - "token_endpoint_auth_method": [ - "client_secret_basic", - "client_secret_post" - ] - }, - "issuer": "https://127.0.0.1:8000/", - "redirect_uris": [ - "https://{domain}:{port}/authz_cb/django" - ], - "post_logout_redirect_uris": [ - "https://{domain}:{port}/session_logout/django" - ], - "frontchannel_logout_uri": "https://{domain}:{port}/fc_logout/django", - "frontchannel_logout_session_required": true, - "backchannel_logout_uri": "https://{domain}:{port}/bc_logout/django", - "backchannel_logout_session_required": true, - "services": { - "discovery": { - "class": "oidcrp.oidc.provider_info_discovery.ProviderInfoDiscovery", - "kwargs": {} - }, - "registration": { - "class": "oidcrp.oidc.registration.Registration", - "kwargs": {} - }, - "authorization": { - "class": "oidcrp.oidc.authorization.Authorization", - "kwargs": {} - }, - "accesstoken": { - "class": "oidcrp.oidc.access_token.AccessToken", - "kwargs": {} - }, - "userinfo": { - "class": "oidcrp.oidc.userinfo.UserInfo", - "kwargs": {} - }, - "end_session": { - "class": "oidcrp.oidc.end_session.EndSession", - "kwargs": {} - } - }, - "add_ons": { - "pkce": { - "function": "oidcrp.oauth2.add_on.pkce.add_support", - "kwargs": { - "code_challenge_length": 64, - "code_challenge_method": "S256" - } - } - } - } - }, - "webserver": { - "port": 8090, - "domain": "127.0.0.1", - "server_cert": "certs/cert.pem", - "server_key": "certs/key.pem", - "debug": true - } -} diff --git a/example/django_op/example/example/__init__.py b/example/django_op/example/example/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/django_op/example/example/oidc_op.conf.yaml b/example/django_op/example/example/oidc_op.conf.yaml deleted file mode 100644 index 955e7de4..00000000 --- a/example/django_op/example/example/oidc_op.conf.yaml +++ /dev/null @@ -1,329 +0,0 @@ -# TODO: the following four should be handled in settings.py -# session_cookie_name: django_oidc_op -base_url: &base_url 'https://127.0.0.1:8000' -#base_data_path: 'data/oidc_op/' - -key_def: &key_def - - - type: RSA - use: - - sig - - - type: EC - crv: "P-256" - use: - - sig - -OIDC_KEYS: &oidc_keys - 'private_path': data/oidc_op/private/jwks.json - 'key_defs': *key_def - 'public_path': data/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' - -op: - server_info: - issuer: *base_url - httpc_params: - verify: False - seed: asdasdasdasd-random - session_key: - filename: data/oidc_op/private/session_jwks.json - type: OCT - use: sig - capabilities: - # indicates that unknow/unavailable scopes requested by a RP - # would get a 403 error message instead of be declined implicitly. - # If False the op will only release the available scopes and ignoring the missings. - # Default to False - #deny_unknown_scopes: true - subject_types_supported: - - public - - pairwise - grant_types_supported: - - authorization_code - - implicit - - urn:ietf:params:oauth:grant-type:jwt-bearer - - refresh_token - service_documentation: https://that.url.org/service_documentation - ui_locales_supported: [en-US, it-IT] - op_policy_uri: https://that.url.org/op_policy_uri - op_tos_uri: https://that.url.org/op_tos_uri - id_token: - class: oidcendpoint.id_token.IDToken - kwargs: - base_claims: - email: - essential: True - email_verified: - essential: True - token_handler_args: - jwks_def: - private_path: data/oidc_op/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 - token: - class: oidcendpoint.token.jwt_token.JWTToken - lifetime: 3600 - add_claims: - - email - - email_verified - - phone_number - - phone_number_verified - add_claim_by_scope: True - aud: - - *base_url - refresh: - lifetime: 86400 - keys: - *oidc_keys - - template_dir: oidc_op/templates - template_handler: django.template.Template - - endpoint: - webfinger: - path: '.well-known/webfinger' - class: oidcendpoint.oidc.discovery.Discovery - kwargs: - # TODO: optionally manage discovery service authn - client_authn_method: null - provider_info: - path: ".well-known/openid-configuration" - class: oidcendpoint.oidc.provider_config.ProviderConfiguration - kwargs: - # TODO: optionally manage openid-configuration authn - client_authn_method: null - registration: - path: registration - class: oidcendpoint.oidc.registration.Registration - kwargs: - # TODO: make a authn method for 'client_authn_method' - client_authn_method: null - client_secret_expiration_time: 432000 - # this way the secret will never expire. - # client_secret_expires: False - registration_api: - path: registration_api - class: oidcendpoint.oidc.read_registration.RegistrationRead - kwargs: - client_authn_method: - - bearer_header - introspection: - path: introspection - class: oidcendpoint.oauth2.introspection.Introspection - kwargs: - client_authn_method: - client_secret_post: ClientSecretPost - release: - - username - authorization: - path: authorization - class: oidcendpoint.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: oidcendpoint.oidc.token.Token - kwargs: - allow_refresh: False - client_authn_method: - - client_secret_post - - client_secret_basic - - client_secret_jwt - - private_key_jwt - userinfo: - path: userinfo - class: oidcendpoint.oidc.userinfo.UserInfo - kwargs: - claim_types_supported: - - normal - - aggregated - - distributed - end_session: - path: session - class: oidcendpoint.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: oidc_op.users.UserInfo - kwargs: - # map claims to django user attributes here: - claims_map: - phone_number: telephone - family_name: last_name - given_name: first_name - email: email - verified_email: email - gender: gender - birthdate: get_oidc_birthdate - updated_at: get_oidc_lastlogin - authentication: - user: - acr: oidcendpoint.user_authn.authn_context.INTERNETPROTOCOLPASSWORD - class: oidc_op.users.UserPassDjango - kwargs: - # this would override web resource where credentials will be POSTed - # append the trailing slash or add APPEND_SLASH=False in Django settings.py - verify_endpoint: 'verify/oidc_user_login/' - template: oidc_login.html - - # args1: - # class: oidcendpoint.util.JSONDictDB - # kwargs: - # args1_1: data/oidc_op/things.json - - page_header: "Testing log in" - submit_btn: "Get me in!" - user_label: "Nickname" - passwd_label: "Secret sauce" - #anon: - #acr: oidcendpoint.user_authn.authn_context.UNSPECIFIED - #class: oidcendpoint.user_authn.user.NoAuthn - #kwargs: - #user: thatusername - # cookie_dealer: - # class: oidcendpoint.cookie.CookieDealer - # kwargs: - # these should be updated... - # sign_jwk: - # filename: 'data/oidc_op/private/cookie_sign_jwk.json' - # type: OCT - # kid: cookie_sign_key_id - # enc_jwk: - ## otherwise do it yourself: jwkgen --kty SYM > data/oidc_op/private/cookie_enc_jwk.json - # filename: 'data/oidc_op/private/cookie_enc_jwk.json' - # type: OCT - # kid: cookie_enc_key_id - - ## the manual one here... - # sign_jwk: data/oidc_op/private/cookie_sign_jwk.json - # sign_alg: 'SHA256' - - # ## jwkgen --kty SYM > data/oidc_op/private/cookie_enc_jwk.json - # enc_jwk: 'data/oidc_op/private/cookie_enc_jwk.json' - - # default_values: - # name: oidc_op - # domain: *base_url - # path: / - # max_age: 3600 - - login_hint2acrs: - class: oidcendpoint.login_hint.LoginHint2Acrs - kwargs: - scheme_map: - email: - - oidcendpoint.user_authn.authn_context.INTERNETPROTOCOLPASSWORD - - # this adds PKCE support as mandatory - disable it if needed (essential: False) - add_on: - pkce: - function: oidcendpoint.oidc.add_on.pkce.add_pkce_support - kwargs: - essential: True - code_challenge_method: - #plain - S256 - S384 - S512 - claims: - function: oidcendpoint.oidc.add_on.custom_scopes.add_custom_scopes - kwargs: - # that_nice_scope: - # - email - research_and_scholarship: - - name - - given_name - - family_name - - email - - email_verified - - sub - - iss - - eduperson_scoped_affiliation - - db_conf: - # abstract_storage_cls: oidcmsg.storage.extension.LabeledAbstractStorage - keyjar: - handler: oidcmsg.storage.abfile.LabeledAbstractFileSystem - fdir: storage/keyjar - key_conv: oidcmsg.storage.converter.QPKey - value_conv: cryptojwt.serialize.item.KeyIssuer - label: 'keyjar' - default: - handler: oidcmsg.storage.abfile.LabeledAbstractFileSystem - fdir: storage/default - key_conv: oidcmsg.storage.converter.QPKey - value_conv: oidcmsg.storage.converter.JSON - client: - handler: oidc_op.db_interfaces.OidcClientDb - # state: - # handler: oidcmsg.storage.abfile.LabeledAbstractFileSystem - # fdir: storage/state - # key_conv: oidcmsg.storage.converter.QPKey - # value_conv: oidcmsg.storage.converter.JSON - jti: - handler: oidcmsg.storage.abfile.LabeledAbstractFileSystem - fdir: storage/jti - key_conv: oidcmsg.storage.converter.QPKey - value_conv: oidcmsg.storage.converter.JSON - session: - # handler: oidcmsg.storage.abfile.AbstractFileSystem - handler: oidc_op.db_interfaces.OidcSessionDb - fdir: storage/session - key_conv: oidcmsg.storage.converter.QPKey - value_conv: oidcmsg.storage.converter.JSON - sso: - # handler: oidcmsg.storage.abfile.AbstractFileSystem - handler: oidc_op.db_interfaces.OidcSsoDb - fdir: storage/sso - key_conv: oidcmsg.storage.converter.QPKey - value_conv: oidcmsg.storage.converter.JSON - -# not used with gunicorn or any other production server ... -# 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: *domain - # debug: true diff --git a/example/django_op/example/example/oidc_provider_settings.py b/example/django_op/example/example/oidc_provider_settings.py deleted file mode 100644 index 8a887f77..00000000 --- a/example/django_op/example/example/oidc_provider_settings.py +++ /dev/null @@ -1,354 +0,0 @@ -OIDCOP_CONF = { - "port": 8000, - "domain": "127.0.0.1", - "server_name": "{domain}:{port}", - "base_url": "https://{domain}:{port}", - "key_def": [ - { - "type": "RSA", - "use": [ - "sig" - ] - }, - { - "type": "EC", - "crv": "P-256", - "use": [ - "sig" - ] - } - ], - "OIDC_KEYS": { - "private_path": "data/oidc_op/private/jwks.json", - "key_defs": [ - { - "type": "RSA", - "use": [ - "sig" - ] - }, - { - "type": "EC", - "crv": "P-256", - "use": [ - "sig" - ] - } - ], - "public_path": "data/static/jwks.json", - "read_only": False, - "uri_path": "static/jwks.json" - }, - "op": { - # "seed": "CHANGE-THIS-RANDOMNESS!!!", - "server_info": { - "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": "oidc_provider.users.UserPassDjango", - "kwargs": { - "verify_endpoint": "verify/oidc_user_login/", - "template": "oidc_login.html", - - "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" - ], - # indicates that unknow/unavailable scopes requested by a RP - # would get a 403 error message instead of be declined implicitly. - # If False the op will only release the available scopes and ignoring the missings. - # Default to False - #deny_unknown_scopes: True - }, - "cookie_handler": { - "class": "oidcop.cookie_handler.CookieHandler", - "kwargs": { - "keys": { - "private_path": "data/oidc_op/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": None - } - }, - "provider_info": { - "path": ".well-known/openid-configuration", - "class": "oidcop.oidc.provider_config.ProviderConfiguration", - "kwargs": { - "client_authn_method": None - } - }, - "registration": { - "path": "registration", - "class": "oidcop.oidc.registration.Registration", - "kwargs": { - "client_authn_method": None, - "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": None, - "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 - }, - "issuer": "https://{domain}:{port}", - "keys": { - "private_path": "data/oidc_op/private/jwks.json", - "key_defs": [ - { - "type": "RSA", - "use": [ - "sig" - ] - }, - { - "type": "EC", - "crv": "P-256", - "use": [ - "sig" - ] - } - ], - "public_path": "data/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": "data/oidc_op/private/session_jwk.json", - "type": "OCT", - "use": "sig" - }, - "template_dir": "templates", - "token_handler_args": { - "jwks_def": { - "private_path": "data/oidc_op/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": "oidc_provider.users.UserInfo", - "kwargs": { - # map claims to django user attributes here: - "claims_map": { - "phone_number": "telephone", - "family_name": "last_name", - "given_name": "first_name", - "email": "email", - "verified_email": "email", - "gender": "gender", - "birthdate": "get_oidc_birthdate", - "updated_at": "get_oidc_lastlogin" - } - } - } - } - } - -} diff --git a/example/django_op/example/example/settings.py b/example/django_op/example/example/settings.py deleted file mode 100644 index 828b452b..00000000 --- a/example/django_op/example/example/settings.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -Django settings for example project. - -Generated by 'django-admin startproject' using Django 2.2.5. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.2/ref/settings/ -""" - -import os - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'hm8_+266i)9@*9#ncep59m+mp$_+8d_#19rd5x0ujtwlp140s@' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ['*'] - - -# Application definition - -INSTALLED_APPS = [ - 'accounts', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'oidc_provider', - -] - -if 'oidc_provider' in INSTALLED_APPS: - from . oidc_provider_settings import OIDCOP_CONF - - - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'example.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'example.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -AUTH_USER_MODEL = "accounts.User" -# Password validation -# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/2.2/topics/i18n/ - -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_L10N = True -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.2/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'data/static') - -# LOGGING -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - # exact format is not important, this is the minimum information - 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - }, - 'detailed': { - 'format': '[%(asctime)s] %(message)s [(%(levelname)s) %(name)s.%(funcName)s:%(lineno)s]' - }, - }, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - }, - 'console': { - 'formatter': 'detailed', - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - }, - }, - 'loggers': { - 'django_test': { - 'handlers': ['console'], - 'level': 'DEBUG', - }, - 'django': { - 'handlers': ['console', 'mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - # 'django.server': { - # 'handlers': ['console'], - # 'level': 'INFO', - # 'propagate': False, - # }, - 'oidc_provider': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': True, - }, - 'oidc_op': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': False, - }, - # 'oidcop.endpoint_context': { - # 'handlers': ['console', 'mail_admins'], - # 'level': 'DEBUG', - # 'propagate': False, - # }, - # 'oidcop.sso_db': { - # 'handlers': ['console', 'mail_admins'], - # 'level': 'DEBUG', - # 'propagate': False, - # }, - # 'oidcop.session': { - # 'handlers': ['console', 'mail_admins'], - # 'level': 'DEBUG', - # 'propagate': False, - # }, - # 'oidcmsg': { - # 'handlers': ['console', 'mail_admins'], - # 'level': 'INFO', - # 'propagate': False, - # }, - } -} diff --git a/example/django_op/example/example/urls.py b/example/django_op/example/example/urls.py deleted file mode 100644 index 69c9bef1..00000000 --- a/example/django_op/example/example/urls.py +++ /dev/null @@ -1,30 +0,0 @@ -"""example URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/2.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.conf import settings -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import include, path - -urlpatterns = [ - path('admin/', admin.site.urls), -] - -urlpatterns += static(settings.STATIC_URL, - document_root=settings.STATIC_ROOT) - -if 'oidc_provider' in settings.INSTALLED_APPS: - import oidc_provider.urls - urlpatterns += path('', include((oidc_provider.urls, 'oidc_op',))), diff --git a/example/django_op/example/example/wsgi.py b/example/django_op/example/example/wsgi.py deleted file mode 100644 index f18f865c..00000000 --- a/example/django_op/example/example/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for example project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') - -application = get_wsgi_application() diff --git a/example/django_op/example/manage.py b/example/django_op/example/manage.py deleted file mode 100755 index 2f9e225a..00000000 --- a/example/django_op/example/manage.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/example/django_op/example/oidc_provider b/example/django_op/example/oidc_provider deleted file mode 120000 index 6e773bec..00000000 --- a/example/django_op/example/oidc_provider +++ /dev/null @@ -1 +0,0 @@ -../oidc_provider \ No newline at end of file diff --git a/example/django_op/example/requirements.txt b/example/django_op/example/requirements.txt deleted file mode 100644 index fbb2b950..00000000 --- a/example/django_op/example/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pycountry diff --git a/example/django_op/example/run.bash b/example/django_op/example/run.bash deleted file mode 100644 index 113f6617..00000000 --- a/example/django_op/example/run.bash +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -./manage.py migrate -./manage.py collectstatic --no-input - -gunicorn example.wsgi -b0.0.0.0:8000 --keyfile=./data/oidc_op/certs/key.pem --certfile=./data/oidc_op/certs/cert.pem --reload --timeout 3600 --capture-output --enable-stdio-inheritance - diff --git a/example/django_op/oidc-op.dev.notes.md b/example/django_op/oidc-op.dev.notes.md deleted file mode 100644 index 83dbc9d3..00000000 --- a/example/django_op/oidc-op.dev.notes.md +++ /dev/null @@ -1,260 +0,0 @@ -# Provider discovery - -```` -endpoint -http_info -req_args.__dict__['_dict'] -current_app.server.endpoint_context.cdb -current_app.server.endpoint_context.session_manager.dump() - - -{'db': {}, 'salt': 'P3e1EPrBvoml1VDE8hBHXzALYI0AsMUP'} -```` - -# useful hints ... -- http_info -- req_args.to_json() -- req_args.__dict__['_dict'] - - -# Registration - -Dynamic client registration endpoint - -```` -endpoint -http_info -req_args.__dict__['_dict'] -current_app.server.endpoint_context.cdb -current_app.server.endpoint_context.session_manager.dump() - - - - -{'86M1io6O2Vdy': - {'client_id': '86M1io6O2Vdy', - 'client_salt': 'ehXmVjYE', - 'registration_access_token': 'lRail9TKK3Cj4kZdSt3KDorKVxyQvVGL', - 'registration_client_uri': 'https://127.0.0.1:5000/registration_api?client_id=86M1io6O2Vdy', - 'client_id_issued_at': 1619384394, - 'client_secret': '9f9a5b6dc23daca606c3766a1c6a0de29a2009b007be3d1da7ff8ca5', - 'client_secret_expires_at': 1621976394, - 'application_type': 'web', - 'response_types': ['code'], - 'contacts': ['ops@example.com'], - 'token_endpoint_auth_method': 'client_secret_basic', - 'post_logout_redirect_uris': [('https://127.0.0.1:8090/session_logout/local', '')], - 'jwks_uri': 'https://127.0.0.1:8090/static/jwks.json', - 'frontchannel_logout_uri': 'https://127.0.0.1:8090/fc_logout/local', - 'frontchannel_logout_session_required': True, - 'backchannel_logout_uri': 'https://127.0.0.1:8090/bc_logout/local', - 'grant_types': ['authorization_code'], - 'redirect_uris': [('https://127.0.0.1:8090/authz_cb/local', {})] - } -} - - - -{'db': {}, 'salt': 'P3e1EPrBvoml1VDE8hBHXzALYI0AsMUP'} -```` - -# Authorization endpont - -```` -http_info - -endpoint; current_app.server.endpoint_context.session_manager.dump() - - - -```` - - -Session dump -```` -{'eWM0Hi7tcdJ5': {'client_id': 'eWM0Hi7tcdJ5', 'client_salt': 'mb45L2cF', 'registration_access_token': 'Tob3Jw0hZ29yqd2HMJj7VhdF98G6jnqu', 'registration_client_uri': 'https://127.0.0.1:5000/registration_api?client_id=eWM0Hi7tcdJ5', 'client_id_issued_at': 1619260359, 'client_secret': 'a7439bd659c5058dbe667a1a5f6c837336f31102d35d435e9f090a2e', 'client_secret_expires_at': 1621852359, 'application_type': 'web', 'response_types': ['code'], 'contacts': ['ops@example.com'], 'token_endpoint_auth_method': 'client_secret_basic', 'post_logout_redirect_uris': [('https://127.0.0.1:8090/session_logout/local', '')], 'jwks_uri': 'https://127.0.0.1:8090/static/jwks.json', 'frontchannel_logout_uri': 'https://127.0.0.1:8090/fc_logout/local', 'frontchannel_logout_session_required': True, 'backchannel_logout_uri': 'https://127.0.0.1:8090/bc_logout/local', 'grant_types': ['authorization_code'], 'redirect_uris': [('https://127.0.0.1:8090/authz_cb/local', {})]}} -{'db': {}, 'salt': 'P3e1EPrBvoml1VDE8hBHXzALYI0AsMUP'} -```` - -# Token endpoint - -```` -http_info - -endpoint; current_app.server.endpoint_context.session_manager.dump() - -# current_app.server.endpoint_context.cdb not changes from the previous ... - -# session dump -{ - "db": { - "diana": [ - "oidcop.session.info.UserSessionInfo", - { - "subordinate": [ - "86M1io6O2Vdy" - ], - "revoked": false, - "type": "UserSessionInfo", - "extra_args": {}, - "user_id": "diana" - } - ], - "diana;;86M1io6O2Vdy": [ - "oidcop.session.info.ClientSessionInfo", - { - "subordinate": [ - "fcc1c962a60911eb9d4d57d896f78a5d" - ], - "revoked": false, - "type": "ClientSessionInfo", - "extra_args": {}, - "client_id": "86M1io6O2Vdy" - } - ], - "diana;;86M1io6O2Vdy;;fcc1c962a60911eb9d4d57d896f78a5d": [ - "oidcop.session.grant.Grant", - { - "expires_at": 1619427939, - "issued_at": 1619384739, - "not_before": 0, - "revoked": false, - "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" - ] - } - }, - "used": 2, - "authentication_event": { - "oidcop.authn_event.AuthnEvent": { - "uid": "diana", - "authn_info": "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD", - "authn_time": 1619384739, - "valid_until": 1619388339 - } - }, - "authorization_request": { - "oidcmsg.oidc.AuthorizationRequest": { - "redirect_uri": "https://127.0.0.1:8090/authz_cb/local", - "scope": "openid profile email address phone", - "response_type": "code", - "nonce": "TXwiaGM9I8kEB4BbC4nqHNWc", - "state": "uKZM2ciKxWbg4x4xtsltzoy4PvjoQf4T", - "code_challenge": "WYVBXCNsPiDTe0lClNPG69qRB_yl6mJ2Lwop9XWjhYA", - "code_challenge_method": "S256", - "client_id": "86M1io6O2Vdy" - } - }, - "claims": { - "userinfo": { - "sub": null, - "name": null, - "given_name": null, - "family_name": null, - "middle_name": null, - "nickname": null, - "profile": null, - "picture": null, - "website": null, - "gender": null, - "birthdate": null, - "zoneinfo": null, - "locale": null, - "updated_at": null, - "preferred_username": null, - "email": null, - "email_verified": null, - "address": null, - "phone_number": null, - "phone_number_verified": null - }, - "introspection": {}, - "id_token": {}, - "access_token": {} - }, - "issued_token": [ - { - "expires_at": 0, - "issued_at": 1619384739, - "not_before": 0, - "revoked": false, - "usage_rules": { - "supports_minting": [ - "access_token", - "refresh_token", - "id_token" - ], - "max_usage": 1 - }, - "used": 1, - "claims": {}, - "id": "fcc1c963a60911eb9d4d57d896f78a5d", - "name": "AuthorizationCode", - "resources": [], - "scope": [], - "type": "authorization_code", - "value": "Z0FBQUFBQmdoZG1qdTlNZ0hzNGZzcVhZNnRJTXE2bkZZNGlVeXZTaDYwWlV4Vm1yMnFQWUZSSVFmb25HQjluQy1ZVWNXTWJlZ082OE03dVB1NmdBVG8xbkwxV1BWRTBZVkIzYXctY0xhTDB6c2hXUzhmeTRBNE9Ua3RxVVlmU0dDSElPeUJRb1VHQndtT21PR25nRWx3QXdoSG1DdklFM0REdjhWa2I2bWNtQzhFazdrRzBybWd4VV9oX19hcEt4MDZ3Uk5lNGpvbXllMVVmNkt4VXNRaW1FVHRTdS13ajVxczVibmtaXzRhXzhMcW9DOEFXVGtZND0=" - }, - { - "expires_at": 0, - "issued_at": 1619384739, - "not_before": 0, - "revoked": false, - "usage_rules": {}, - "used": 0, - "based_on": "Z0FBQUFBQmdoZG1qdTlNZ0hzNGZzcVhZNnRJTXE2bkZZNGlVeXZTaDYwWlV4Vm1yMnFQWUZSSVFmb25HQjluQy1ZVWNXTWJlZ082OE03dVB1NmdBVG8xbkwxV1BWRTBZVkIzYXctY0xhTDB6c2hXUzhmeTRBNE9Ua3RxVVlmU0dDSElPeUJRb1VHQndtT21PR25nRWx3QXdoSG1DdklFM0REdjhWa2I2bWNtQzhFazdrRzBybWd4VV9oX19hcEt4MDZ3Uk5lNGpvbXllMVVmNkt4VXNRaW1FVHRTdS13ajVxczVibmtaXzRhXzhMcW9DOEFXVGtZND0=", - "claims": {}, - "id": "fcc4fc72a60911eb9d4d57d896f78a5d", - "name": "AccessToken", - "resources": [], - "scope": [], - "type": "access_token", - "value": "eyJhbGciOiJFUzI1NiIsImtpZCI6IlNWUXpPV1ZVUm1oNWIxcHVVVmx1UlY4dGVVUlpVVlZTZFhkcFdVUTJTbTVMY1U0M01EWm1WV2REVlEifQ.eyJzY29wZSI6IFsib3BlbmlkIiwgInByb2ZpbGUiLCAiZW1haWwiLCAiYWRkcmVzcyIsICJwaG9uZSJdLCAiYXVkIjogWyI4Nk0xaW82TzJWZHkiXSwgInNpZCI6ICJkaWFuYTs7ODZNMWlvNk8yVmR5OztmY2MxYzk2MmE2MDkxMWViOWQ0ZDU3ZDg5NmY3OGE1ZCIsICJ0dHlwZSI6ICJUIiwgImlzcyI6ICJodHRwczovLzEyNy4wLjAuMTo1MDAwIiwgImlhdCI6IDE2MTkzODQ3MzksICJleHAiOiAxNjE5Mzg4MzM5fQ.Brva_I8bBM5z_1ZxFBWSRFN3U95y_YQxnLG5-51NrUmu862M-KSj4kd5v5vFGHiHF0iFvBuDLD6pSZL1RHXHCg" - } - ], - "resources": [ - "86M1io6O2Vdy" - ], - "scope": [ - "openid", - "profile", - "email", - "address", - "phone" - ], - "sub": "93be77e1b212f1643e0ee9dd5e477e2a2a231dc6ca22dd3273345e63eb156a23" - } - ], - "8ea62b28f57646fe8db31b4bdea0e262": [ - "oidcop.session.info.SessionInfo", - { - "subordinate": [], - "revoked": false, - "type": "", - "extra_args": {} - } - ] - }, - "salt": "1Kih63fBe5ympYSWi5z2aVXXCVKxqMvN" -} - -```` - -# Userinfo endpoint - -```` - -{'db': {'diana': ['oidcop.session.info.UserSessionInfo', {'subordinate': ['eWM0Hi7tcdJ5'], 'revoked': False, 'type': 'UserSessionInfo', 'extra_args': {}, 'user_id': 'diana'}], 'diana;;eWM0Hi7tcdJ5': ['oidcop.session.info.ClientSessionInfo', {'subordinate': ['c75b0e0ea4e811eba57a51f2252cef26'], 'revoked': False, 'type': 'ClientSessionInfo', 'extra_args': {}, 'client_id': 'eWM0Hi7tcdJ5'}], 'diana;;eWM0Hi7tcdJ5;;c75b0e0ea4e811eba57a51f2252cef26': ['oidcop.session.grant.Grant', {'expires_at': 0, 'issued_at': 1619260524, 'not_before': 0, 'revoked': False, 'usage_rules': {}, 'used': 2, 'authentication_event': {'oidcop.authn_event.AuthnEvent': {'uid': 'diana', 'authn_info': 'oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD', 'authn_time': 1619260524, 'valid_until': 1619264124}}, 'authorization_request': {'oidcmsg.oidc.AuthorizationRequest': {'redirect_uri': 'https://127.0.0.1:8090/authz_cb/local', 'scope': 'openid profile email address phone', 'response_type': 'code', 'nonce': 'xgR3dwSaW6s2q7sJ7Ar5KGEJ', 'state': 'AxNY1bnoRd5xCmTYDQIGRlq3XUCMEXoP', 'code_challenge': 'tfXn2btZVqbzkrSkyPUw1jAjtQXH2M2fUgLyqSMS0ak', 'code_challenge_method': 'S256', 'client_id': 'eWM0Hi7tcdJ5'}}, 'claims': {}, 'issued_token': [{'expires_at': 0, 'issued_at': 1619260524, 'not_before': 0, 'revoked': False, 'usage_rules': {'supports_minting': ['access_token', 'refresh_token'], 'max_usage': 1}, 'used': 1, 'claims': {}, 'id': 'c75b0e0fa4e811eba57a51f2252cef26', 'name': 'AuthorizationCode', 'resources': [], 'scope': [], 'type': 'authorization_code', 'value': 'Z0FBQUFBQmdnX1JzTEZ0ZDV0LWpic1RvYm95cEUtNG1BTV9XTzZyaFV1RW1rM2ppMnNCVThzb3RfemVMQ0hSd296Y1VyVF9OUXBXNGVRNjVjYkRqMl9leF9sQ2xnY3h3ZWh4X1FFeXFLMlhDZE9NTWtEcFZkU3RXNURTbTRrZ1Q5dEh5TWZrVlhmYnU2N0dwenBlM2J1WlNpYzY4cWRjTHUzYXZvWEc2TG0zSEtnY3ZKMGlvOFF4X19pWks0Zl9DUTRuU09ndnRNRTdJRmtNZ2NqU09aWDcxUlhjdl8tZmd6Z1NNcWViS1FjQjdnMGlZN21xcVJnRT0='}, {'expires_at': 0, 'issued_at': 1619260756, 'not_before': 0, 'revoked': False, 'usage_rules': {}, 'used': 0, 'based_on': 'Z0FBQUFBQmdnX1JzTEZ0ZDV0LWpic1RvYm95cEUtNG1BTV9XTzZyaFV1RW1rM2ppMnNCVThzb3RfemVMQ0hSd296Y1VyVF9OUXBXNGVRNjVjYkRqMl9leF9sQ2xnY3h3ZWh4X1FFeXFLMlhDZE9NTWtEcFZkU3RXNURTbTRrZ1Q5dEh5TWZrVlhmYnU2N0dwenBlM2J1WlNpYzY4cWRjTHUzYXZvWEc2TG0zSEtnY3ZKMGlvOFF4X19pWks0Zl9DUTRuU09ndnRNRTdJRmtNZ2NqU09aWDcxUlhjdl8tZmd6Z1NNcWViS1FjQjdnMGlZN21xcVJnRT0=', 'claims': {}, 'id': '519d54e6a4e911eba57a51f2252cef26', 'name': 'AccessToken', 'resources': [], 'scope': [], 'type': 'access_token', 'value': 'eyJhbGciOiJFUzI1NiIsImtpZCI6IlNWUXpPV1ZVUm1oNWIxcHVVVmx1UlY4dGVVUlpVVlZTZFhkcFdVUTJTbTVMY1U0M01EWm1WV2REVlEifQ.eyJzY29wZSI6IFtdLCAiYXVkIjogW10sICJzaWQiOiAiZGlhbmE7O2VXTTBIaTd0Y2RKNTs7Yzc1YjBlMGVhNGU4MTFlYmE1N2E1MWYyMjUyY2VmMjYiLCAidHR5cGUiOiAiVCIsICJpc3MiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6NTAwMCIsICJpYXQiOiAxNjE5MjYwNzU2LCAiZXhwIjogMTYxOTI2NDM1Nn0.j-YSAF7M6naaq2w8ntPOi-55shCIpWWFKmluYS18wkPrp5L5NFViuhmhLRY1CHr_xtbWv944Ud06m0RKP7Gd0Q'}], 'resources': [], 'scope': [], 'sub': '8fb4f8ee2bad3d54e58fcc2bb4f56200391427fb587bbcb95ca535cd818fd914'}], 'c6171c52f7dd4dcfa51e28f9af5833fd': ['oidcop.session.info.SessionInfo', {'subordinate': [], 'revoked': False, 'type': '', 'extra_args': {}}]}, 'salt': 'P3e1EPrBvoml1VDE8hBHXzALYI0AsMUP'} -```` diff --git a/example/django_op/oidc_provider/__init__.py b/example/django_op/oidc_provider/__init__.py deleted file mode 100644 index 4bd5dbb5..00000000 --- a/example/django_op/oidc_provider/__init__.py +++ /dev/null @@ -1 +0,0 @@ -default_app_config = 'oidc_provider.apps.OidcOpConfig' diff --git a/example/django_op/oidc_provider/admin.py b/example/django_op/oidc_provider/admin.py deleted file mode 100644 index 7a3c7456..00000000 --- a/example/django_op/oidc_provider/admin.py +++ /dev/null @@ -1,214 +0,0 @@ -import logging - -from django import forms -from django.contrib import admin -from django.contrib.sessions.models import Session -from django.utils.safestring import mark_safe - -from . models import * -from . utils import decode_token - - -logger = logging.getLogger(__name__) - - -class OidcRPContactModelForm(forms.ModelForm): - class Meta: - model = OidcRPContact - fields = ('__all__') - - -class OidcRPContactInline(admin.TabularInline): - model = OidcRPContact - form = OidcRPContactModelForm - extra = 0 - - -class OidcRPRedirectUriModelForm(forms.ModelForm): - class Meta: - model = OidcRPRedirectUri - fields = ('__all__') - - -class OidcRPRedirectUriInline(admin.TabularInline): - model = OidcRPRedirectUri - form = OidcRPRedirectUriModelForm - extra = 0 - - -class OidcRPGrantTypeModelForm(forms.ModelForm): - class Meta: - model = OidcRPGrantType - fields = ('__all__') - - -class OidcRPGrantTypeInline(admin.TabularInline): - model = OidcRPGrantType - form = OidcRPGrantTypeModelForm - extra = 0 - - -class OidcRPResponseTypeModelForm(forms.ModelForm): - class Meta: - model = OidcRPResponseType - fields = ('__all__') - - -class OidcRPResponseTypeInline(admin.TabularInline): - model = OidcRPResponseType - form = OidcRPResponseTypeModelForm - extra = 0 - - -class OidcRPScopeModelForm(forms.ModelForm): - class Meta: - model = OidcRPScope - fields = ('__all__') - - -class OidcRPScopeInline(admin.TabularInline): - model = OidcRPScope - form = OidcRPScopeModelForm - extra = 0 - - -@admin.register(OidcRelyingParty) -class OidcRelyingPartyAdmin(admin.ModelAdmin): - list_filter = ('created', 'modified', 'is_active') - list_display = ('client_id', 'created', - 'last_seen', 'is_active') - search_fields = ('client_id',) - list_editable = ('is_active',) - inlines = (OidcRPScopeInline, - OidcRPResponseTypeInline, - OidcRPGrantTypeInline, - OidcRPContactInline, - OidcRPRedirectUriInline) - fieldsets = ( - (None, { - 'fields': ( - ('client_id', 'client_secret',), - ('client_salt', 'jwks_uri'), - ('registration_client_uri',), - ('registration_access_token',), - ('application_type', - 'token_endpoint_auth_method'), - ('is_active', ) - ) - }, - ), - ('Temporal values', - { - 'fields': ( - (('client_id_issued_at', - 'client_secret_expires_at', - 'last_seen')), - - ), - - }, - ), - ) - - # def save_model(self, request, obj, form, change): - # res = False - # msg = '' - # try: - # json.dumps(obj.as_pysaml2_mdstore_row()) - # res = obj.validate() - # super(MetadataStoreAdmin, self).save_model(request, obj, form, change) - # except Exception as excp: - # obj.is_valid = False - # obj.save() - # msg = str(excp) - - # if not res: - # messages.set_level(request, messages.ERROR) - # _msg = _("Storage {} is not valid, if 'mdq' at least a " - # "valid url must be inserted. " - # "If local: at least a file or a valid path").format(obj.name) - # if msg: _msg = _msg + '. ' + msg - # messages.add_message(request, messages.ERROR, _msg) - - -@admin.register(Session) -class SessionAdmin(admin.ModelAdmin): - def _session_data(self, obj): - return obj.get_decoded() - list_display = ['session_key', '_session_data', 'expire_date'] - - -@admin.register(OidcSession) -class OidcSessionAdmin(admin.ModelAdmin): - list_filter = ('created', 'modified', 'valid_until') - list_display = ('client', 'state', 'sso', 'created') - search_fields = ('state', 'sso__user__username') - readonly_fields = ('sid', 'client', 'sso', 'state', 'valid_until', 'info_session_preview', - 'access_token_preview', 'id_token_preview') - - fieldsets = ( - (None, { - 'fields': ( - ('client', ), - ('sso', ), - ('state',), - ('sid',), - ('valid_until',), - ) - }, - ), - ('Session info', - { - 'classes': ('collapse',), - 'fields': ('info_session_preview',), - } - ), - - ('Token previews', - { - 'classes': ('collapse',), - 'fields': ( - ('access_token_preview'), - ('id_token_preview'), - ) - - }, - ), - ) - - def info_session_preview(self, obj): - msg = json.loads(obj.session_info or '{}') - dumps = json.dumps(msg, indent=2) - return mark_safe(dumps.replace('\n', '
').replace('\s', ' ')) - info_session_preview.short_description = 'Info Session preview' - - def access_token_preview(self, obj): - try: - msg = decode_token(obj.session_info or {}, 'access_token') - dumps = json.dumps(msg.to_dict(), indent=2) - return mark_safe(dumps.replace('\n', '
').replace('\s', ' ')) - except Exception as e: - logger.tracelog(e) - access_token_preview.short_description = 'Access Token preview' - - def id_token_preview(self, obj): - try: - msg = decode_token(obj.session_info or {}, 'id_token') - dumps = json.dumps(msg.to_dict(), indent=2) - return mark_safe(dumps.replace('\n', '
').replace('\s', ' ')) - except Exception as e: - logger.tracelog(e) - id_token_preview.short_description = 'ID Token preview' - - class Media: - js = ('js/textarea_autosize.js',) - # css = {'default': ('css/textarea_large.css',)} - - -@admin.register(OidcSessionSso) -class OidcSessionSsoAdmin(admin.ModelAdmin): - list_filter = ('created', 'modified') - list_display = ('user', - 'sub', 'created') - search_fields = ('user',) - readonly_fields = ('sub', 'user') diff --git a/example/django_op/oidc_provider/application.py b/example/django_op/oidc_provider/application.py deleted file mode 100644 index 7dba7f2a..00000000 --- a/example/django_op/oidc_provider/application.py +++ /dev/null @@ -1,49 +0,0 @@ -import logging -import os - -from django.conf import settings -from oidcop.endpoint_context import EndpointContext -from oidcop.server import Server - -from urllib.parse import urlparse -from oidcop.configure import Configuration - -folder = os.path.dirname(os.path.realpath(__file__)) -logger = logging.getLogger(__name__) - - -def init_oidc_op_endpoints(app): - _config = app.srv_config.op - _server_info_config = _config['server_info'] - - iss = _server_info_config['issuer'] - if '{domain}' in iss: - iss = iss.format(domain=app.srv_config.domain, - port=app.srv_config.port) - _server_info_config['issuer'] = iss - - server = Server(_server_info_config, cwd=folder) - - for endp in server.endpoint.values(): - p = urlparse(endp.endpoint_path) - _vpath = p.path.split('/') - if _vpath[0] == '': - endp.vpath = _vpath[1:] - else: - endp.vpath = _vpath - - return server - - -def oidc_provider_init_app(config, name='oidc_op', **kwargs): - name = name or __name__ - app = type('OIDCAppEndpoint', (object,), {"srv_config": config}) - # Initialize the oidc_provider after views to be able to set correct urls - app.endpoint_context = init_oidc_op_endpoints(app) - return app - - -def oidcop_application(conf = settings.OIDCOP_CONF): - config = Configuration(conf = conf) - app = oidc_provider_init_app(config) - return app diff --git a/example/django_op/oidc_provider/apps.py b/example/django_op/oidc_provider/apps.py deleted file mode 100644 index a25c1655..00000000 --- a/example/django_op/oidc_provider/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class OidcOpConfig(AppConfig): - name = 'oidc_provider' - verbose_name = "OpenID Connect Provider" diff --git a/example/django_op/oidc_provider/migrations/0001_initial.py b/example/django_op/oidc_provider/migrations/0001_initial.py deleted file mode 100644 index c4a063df..00000000 --- a/example/django_op/oidc_provider/migrations/0001_initial.py +++ /dev/null @@ -1,155 +0,0 @@ -# Generated by Django 3.1.1 on 2021-04-27 14:11 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='OidcRelyingParty', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('client_id', models.CharField(max_length=255, unique=True)), - ('client_salt', models.CharField(blank=True, max_length=255, null=True)), - ('registration_access_token', models.CharField(blank=True, max_length=255, null=True)), - ('registration_client_uri', models.URLField(blank=True, max_length=255, null=True)), - ('client_id_issued_at', models.DateTimeField(blank=True, null=True)), - ('client_secret', models.CharField(blank=True, help_text='It is not needed for Clients selecting a token_endpoint_auth_method of private_key_jwt', max_length=255, null=True)), - ('client_secret_expires_at', models.DateTimeField(blank=True, help_text='REQUIRED if client_secret is issued', null=True)), - ('application_type', models.CharField(blank=True, default='web', max_length=255, null=True)), - ('token_endpoint_auth_method', models.CharField(blank=True, choices=[('client_secret_post', 'client_secret_post'), ('client_secret_basic', 'client_secret_basic'), ('client_secret_jwt', 'client_secret_jwt'), ('private_key_jwt', 'private_key_jwt')], default='client_secret_basic', max_length=33, null=True)), - ('jwks_uri', models.URLField(blank=True, max_length=255, null=True)), - ('is_active', models.BooleanField(default=True, verbose_name='active')), - ('last_seen', models.DateTimeField(blank=True, null=True)), - ], - options={ - 'verbose_name': 'Relying Party', - 'verbose_name_plural': 'Relying Parties', - }, - ), - migrations.CreateModel( - name='OidcSessionSso', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('sub', models.CharField(blank=True, max_length=255, null=True)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'SSO Session SSO', - 'verbose_name_plural': 'SSO Sessions SSO', - }, - ), - migrations.CreateModel( - name='OidcSession', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('user_uid', models.CharField(max_length=120)), - ('state', models.CharField(blank=True, max_length=255, null=True)), - ('session_info', models.TextField(blank=True, null=True)), - ('grant', models.TextField(blank=True, null=True)), - ('grant_id', models.CharField(blank=True, max_length=255, null=True)), - ('issued_at', models.DateTimeField(blank=True, null=True)), - ('valid_until', models.DateTimeField(blank=True, null=True)), - ('code', models.CharField(blank=True, max_length=1024, null=True)), - ('sid', models.CharField(blank=True, max_length=255, null=True)), - ('sub', models.CharField(blank=True, max_length=255, null=True)), - ('client', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ('sso', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcsessionsso')), - ], - options={ - 'verbose_name': 'SSO Session', - 'verbose_name_plural': 'SSO Sessions', - }, - ), - migrations.CreateModel( - name='OidcRPRedirectUri', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('uri', models.CharField(blank=True, max_length=254, null=True)), - ('values', models.CharField(blank=True, max_length=254, null=True)), - ('type', models.CharField(choices=[('redirect_uris', 'redirect_uris'), ('post_logout_redirect_uris', 'post_logout_redirect_uris')], max_length=33)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ], - options={ - 'verbose_name': 'Relying Party URI', - 'verbose_name_plural': 'Relying Parties URIs', - }, - ), - migrations.CreateModel( - name='OidcRPScope', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('scope', models.CharField(blank=True, max_length=254, null=True)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ], - options={ - 'verbose_name': 'Relying Party Scope', - 'verbose_name_plural': 'Relying Parties Scopes', - 'unique_together': {('client', 'scope')}, - }, - ), - migrations.CreateModel( - name='OidcRPResponseType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('response_type', models.CharField(choices=[('code', 'code'), ('token', 'token'), ('id_token', 'id_token'), ('code token', 'code token'), ('code id_token', 'code id_token'), ('id_token token', 'id_token token'), ('code id_token token', 'code id_token token'), ('none', 'none')], max_length=60)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ], - options={ - 'verbose_name': 'Relying Party Response Type', - 'verbose_name_plural': 'Relying Parties Response Types', - 'unique_together': {('client', 'response_type')}, - }, - ), - migrations.CreateModel( - name='OidcRPGrantType', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('grant_type', models.CharField(choices=[('authorization_code', 'authorization_code'), ('implicit', 'implicit'), ('urn:ietf:params:oauth:grant-type:jwt-bearer', 'urn:ietf:params:oauth:grant-type:jwt-bearer'), ('refresh_token', 'refresh_token')], max_length=60)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ], - options={ - 'verbose_name': 'Relying Party GrantType', - 'verbose_name_plural': 'Relying Parties GrantTypes', - 'unique_together': {('client', 'grant_type')}, - }, - ), - migrations.CreateModel( - name='OidcRPContact', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('contact', models.CharField(blank=True, max_length=254, null=True)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oidc_provider.oidcrelyingparty')), - ], - options={ - 'verbose_name': 'Relying Party Contact', - 'verbose_name_plural': 'Relying Parties Contacts', - 'unique_together': {('client', 'contact')}, - }, - ), - ] diff --git a/example/django_op/oidc_provider/migrations/__init__.py b/example/django_op/oidc_provider/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/django_op/oidc_provider/models.py b/example/django_op/oidc_provider/models.py deleted file mode 100644 index b8fb1aba..00000000 --- a/example/django_op/oidc_provider/models.py +++ /dev/null @@ -1,582 +0,0 @@ -import datetime -import json - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.db import models -from django.utils import timezone -from oidcop.utils import load_yaml_config - - -OIDC_RESPONSE_TYPES = settings.OIDCOP_CONF['op']['server_info'][ - 'endpoint']['authorization']['kwargs']['response_types_supported'] - -OIDC_TOKEN_AUTHN_METHODS = settings.OIDCOP_CONF['op']['server_info'][ - 'endpoint']['token']['kwargs']['client_authn_method'] - -OIDC_GRANT_TYPES = settings.OIDCOP_CONF['op']['server_info']['capabilities']['grant_types_supported'] - -TIMESTAMP_FIELDS = ['client_id_issued_at', 'client_secret_expires_at'] - -# configured in oidcop -# it's not a fixed value, it depends by clients. Here it just references JWTConnect-Python-OidcRP -OIDC_OP_STATE_VALUE_LEN = 32 -OIDC_OP_SID_VALUE_LEN = 56 -OIDC_OP_SUB_VALUE_LEN = 64 - - -# TODO: these test should be improved once oidcop will have specialized objects as values instead of simple strings -def is_state(value): - # USELESS: state is always generated by client/RP! - return len(value) == OIDC_OP_STATE_VALUE_LEN - - -def is_sid(value): - return len(value) == OIDC_OP_SID_VALUE_LEN - - -def is_sub(value): - return len(value) == OIDC_OP_SUB_VALUE_LEN - - -def is_code(value): - # Z0FBQUFBQmZEc1Z5Z1dMRVptX1J6d3AwTDVMdkVtbU1Rcm41VkVVbm03N3pwY21qYlpXc1M0ME1TU25fVlZMdm9MVnFKSW1zb3E4TW1aS0MzeVk4OWF2VjYtZ3FmZ0FXQkluUnVuSEJyWFhtcDFhOEdpTnFiVTdJME1qTFZoWWM2X3lQaGY0VGI0QWZNVUNJc3p6RnRMMWlOUUZzc0gtV3BsdVJvcTBIR3hsbk5SSmV1NVJ0M1N0UXcwV3JLeUR3N1NHYU54U21XVEFpYnBCSnBjN0dYeXFETVByT0J3YnZTSmlqblZSb3JXQmtuazFYdkU3cnNMaz0= - return len(value) > 256 - - -def get_client_by_id(client_id): - client = OidcRelyingParty.objects.filter( - client_id = client_id, - is_active = True - - ) - if client: - return client.last() - - -class TimeStampedModel(models.Model): - """ - An abstract base class model that provides self-updating - ``created`` and ``modified`` fields. - """ - created = models.DateTimeField(auto_now_add=True, editable=False) - modified = models.DateTimeField(auto_now=True, editable=False) - - class Meta: - abstract = True - - -class OidcRelyingParty(TimeStampedModel): - """ - See: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata - - unique if available (check on save): - client_secret should be - registration_access_token unique if available - - issued -> number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time - client_salt -> must be autogenerated on save - """ - _TOKEN_AUTH_CHOICES = ((i, i) for i in OIDC_TOKEN_AUTHN_METHODS) - - client_id = models.CharField( - max_length=255, blank=False, null=False, unique=True - ) - client_salt = models.CharField( - max_length=255, blank=True, null=True - ) - registration_access_token = models.CharField( - max_length=255, blank=True, null=True - ) - registration_client_uri = models.URLField( - max_length=255, blank=True, null=True - ) - client_id_issued_at = models.DateTimeField(blank=True, null=True) - client_secret = models.CharField( - max_length=255, - blank=True, null=True, - help_text=('It is not needed for Clients ' - 'selecting a token_endpoint_auth_method ' - 'of private_key_jwt') - ) - client_secret_expires_at = models.DateTimeField( - blank=True, null=True, - help_text=('REQUIRED if client_secret is issued') - ) - application_type = models.CharField( - max_length=255, blank=True, - null=True, default='web' - ) - token_endpoint_auth_method = models.CharField( - choices=_TOKEN_AUTH_CHOICES, - max_length=33, - blank=True, null=True, - default="client_secret_basic" - ) - jwks_uri = models.URLField(max_length=255, blank=True, null=True) - post_logout_redirect_uris = models.CharField( - max_length=254, blank=True, null=True - ) - redirect_uris = models.CharField( - max_length=254, blank=True, null=True - ) - is_active = models.BooleanField(('active'), default=True) - last_seen = models.DateTimeField(blank=True, null=True) - - @property - def allowed_scopes(self): - scopes = self.oidcrpscope_set.filter(client=self) - if scopes: - return [i.scope for i in scopes] - else: - return ['openid'] - - @allowed_scopes.setter - def allowed_scopes(self, values): - for i in values: - scope = self.oidcrpscope_set.create(client=self, scope=i) - - @property - def contacts(self): - return [elem.contact - for elem in self.oidcrpcontact_set.filter(client=self)] - - @contacts.setter - def contacts(self, values): - old = self.oidcrpcontact_set.filter(client=self) - old.delete() - if isinstance(values, str): - value = [values] - for value in values: - self.oidcrpcontact_set.create(client=self, - contact=value) - - @property - def grant_types(self): - return [elem.grant_type - for elem in - self.oidcrpgranttype_set.filter(client=self)] - - @grant_types.setter - def grant_types(self, values): - old = self.oidcrpgranttype_set.filter(client=self) - old.delete() - if isinstance(values, str): - value = [values] - for value in values: - self.oidcrpgranttype_set.create(client=self, - grant_type=value) - - @property - def response_types(self): - return [ - elem.response_type - for elem in - self.oidcrpresponsetype_set.filter(client=self) - ] - - @response_types.setter - def response_types(self, values): - old = self.oidcrpresponsetype_set.filter(client=self) - old.delete() - if isinstance(values, str): - value = [values] - for value in values: - self.oidcrpresponsetype_set.create( - client=self, response_type=value - ) - - @property - def post_logout_redirect_uris(self): - l = [] - for elem in self.oidcrpredirecturi_set.\ - filter(client=self, type='post_logout_redirect_uris'): - l.append((elem.uri, json.loads(elem.values))) - return l - - @post_logout_redirect_uris.setter - def post_logout_redirect_uris(self, values): - old = self.oidcrpredirecturi_set.filter(client=self) - old.delete() - for value in values: - args = json.dumps(value[1] if value[1] else []) - self.oidcrpredirecturi_set.create( - client=self, - uri=value[0], - values=args, - type='post_logout_redirect_uris' - ) - - @property - def redirect_uris(self): - l = [] - for elem in self.oidcrpredirecturi_set.filter( - client=self, type='redirect_uris'): - l.append((elem.uri, json.loads(elem.values))) - return l - - @redirect_uris.setter - def redirect_uris(self, values): - old = self.oidcrpredirecturi_set.filter(client=self) - old.delete() - for value in values: - self.oidcrpredirecturi_set.create(client=self, - uri=value[0], - values=json.dumps(value[1]), - type='redirect_uris') - - class Meta: - verbose_name = ('Relying Party') - verbose_name_plural = ('Relying Parties') - - def copy(self): - """ - Compability with rohe approach based on dictionaries - """ - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - disabled = ('created', 'modified', 'is_active', 'last_seen') - for dis in disabled: - d.pop(dis) - for key in TIMESTAMP_FIELDS: - if d.get(key): - d[key] = int(datetime.datetime.timestamp(d[key])) - - d['contacts'] = self.contacts - d['grant_types'] = self.grant_types - d['response_types'] = self.response_types - d['post_logout_redirect_uris'] = self.post_logout_redirect_uris - d['redirect_uris'] = self.redirect_uris - d['allowed_scopes'] = self.allowed_scopes - return d - - def __str__(self): - return '{}'.format(self.client_id) - - -class OidcRPResponseType(TimeStampedModel): - client = models.ForeignKey(OidcRelyingParty, on_delete=models.CASCADE) - response_type = models.CharField(choices=[(i, i) for i in OIDC_RESPONSE_TYPES], - max_length=60) - - class Meta: - verbose_name = ('Relying Party Response Type') - verbose_name_plural = ('Relying Parties Response Types') - unique_together = ('client', 'response_type') - - def __str__(self): - return '{}, [{}]'.format(self.client, self.response_type) - - -class OidcRPGrantType(TimeStampedModel): - client = models.ForeignKey(OidcRelyingParty, - on_delete=models.CASCADE) - grant_type = models.CharField(choices=[(i, i) - for i in OIDC_GRANT_TYPES], - max_length=60) - - class Meta: - verbose_name = ('Relying Party GrantType') - verbose_name_plural = ('Relying Parties GrantTypes') - unique_together = ('client', 'grant_type') - - def __str__(self): - return '{}, [{}]'.format(self.client, self.grant_type) - - -class OidcRPContact(TimeStampedModel): - client = models.ForeignKey(OidcRelyingParty, - on_delete=models.CASCADE) - contact = models.CharField(max_length=254, - blank=True, null=True,) - - class Meta: - verbose_name = ('Relying Party Contact') - verbose_name_plural = ('Relying Parties Contacts') - unique_together = ('client', 'contact') - - def __str__(self): - return '{}, [{}]'.format(self.client, self.contact) - - -class OidcRPRedirectUri(TimeStampedModel): - client = models.ForeignKey(OidcRelyingParty, - on_delete=models.CASCADE) - uri = models.CharField(max_length=254, - blank=True, null=True) - values = models.CharField(max_length=254, - blank=True, null=True) - type = models.CharField(choices=(('redirect_uris', 'redirect_uris'), - ('post_logout_redirect_uris', - 'post_logout_redirect_uris')), - max_length=33) - - class Meta: - verbose_name = ('Relying Party URI') - verbose_name_plural = ('Relying Parties URIs') - - def __str__(self): - return '{} [{}] {}'.format(self.client, self.uri, self.type) - - -class OidcRPScope(TimeStampedModel): - client = models.ForeignKey(OidcRelyingParty, - on_delete=models.CASCADE) - scope = models.CharField(max_length=254, - blank=True, null=True,) - - class Meta: - verbose_name = ('Relying Party Scope') - verbose_name_plural = ('Relying Parties Scopes') - unique_together = ('client', 'scope') - - def __str__(self): - return '{}, [{}]'.format(self.client, self.scope) - - -class OidcSessionSso(TimeStampedModel): - """ - """ - - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, - blank=True, null=True) - sub = models.CharField(max_length=255, - blank=True, null=True) - - class Meta: - verbose_name = ('SSO Session SSO') - verbose_name_plural = ('SSO Sessions SSO') - - def get_session(self): - return OidcSession.objects.filter(sso=self).first() - - @property - def state(self): - session = self.get_session() - if session: - return session.state or '' - return '' - - @property - def username(self): - if self.user: - return self.user.username or '' - return '' - - def __iter__(self): - session = self.get_session() - yield session.sid - - def __contains__(self, k): - if getattr(self, k, None): - return True - else: - return False - - def __delitem__(self, name): - OidcSession.objects.filter(sso=self).delete() - self.delete() - - def __getitem__(self, name): - if is_sid(name): - if OidcSession.objects.filter(sid=name, sso=self): - return self - elif name == 'sid': - return self.sid - else: - if OidcSession.objects.filter(state=name, sso=self): - return self - return getattr(self, name) - - def get(self, name, default=None): - return self.__getattribute__(name) - - def __getattribute__(self, name): - if name == 'state': - return self - elif name == 'uid': - return self.user.username - elif name == 'sid': - return self - else: - return models.Model.__getattribute__(self, name) - - def __setattribute__(self, name, value): - if name == 'state': - self.sid = value - return - elif name == 'uid': - user = get_user_model().objects.filter(username=value[0]).first() - self.user = user - self.save() - return - elif name == 'sub': - self.sub = value[0] - self.save() - else: - return models.Model.__setattribute__(self, name, value) - - def __setitem__(self, key, value): - return self.__setattribute__(key, value) - - def append(self, value): - """multiple sid to a sso - """ - if is_sid(value): - if not isinstance(value, list): - value = [value] - - session = self.get_session() - session.sid = value[0] if isinstance(value, list) else value - session.save() - else: - # import pdb; pdb.set_trace() - _msg = '{} .append({}) with missing handler!' - logger.warn(_msg.format(self.__class__.name, value)) - - def __str__(self): - return 'user: {} - sub: {}'.format(self.username, - self.sub) - - -class OidcSession(TimeStampedModel): - """ - Store the session information in this model - """ - user_uid = models.CharField(max_length=120, - blank=False, null=False) - state = models.CharField(max_length=255, - blank=True, null=True) - client = models.ForeignKey(OidcRelyingParty, on_delete=models.CASCADE, - blank=True, null=True) - session_info = models.TextField(blank=True, null=True) - - grant = models.TextField(blank=True, null=True) - grant_id = models.CharField(max_length=255, - blank=True, null=True) - - issued_at = models.DateTimeField(blank=True, null=True) - valid_until = models.DateTimeField(blank=True, null=True) - - sso = models.ForeignKey(OidcSessionSso, on_delete=models.CASCADE, - blank=True, null=True) - code = models.CharField(max_length=1024, - blank=True, null=True) - sid = models.CharField(max_length=255, - blank=True, null=True) - sub = models.CharField(max_length=255, - blank=True, null=True) - - class Meta: - verbose_name = ('SSO Session') - verbose_name_plural = ('SSO Sessions') - - - @classmethod - def create_by(cls, **data): - - if data.get('client_id'): - client = get_client_by_id(data['client_id']) - data['client'] = client - data.pop('client_id') - - if data.get('session_info'): - data['session_info'] = json.dumps(data['session_info'].__dict__) - - res = cls.objects.create(**data) - return res - - - @classmethod - def get_by_sid(cls, value): - sids = cls.objects.filter(sid=value, - valid_until__gt=timezone.localtime()) - if sids: - return sids.last() - - - @classmethod - def get_session_by(cls, **data): - - if data.get('client_id'): - client = get_client_by_id(data['client_id']) - data.pop('client_id') - data['client'] = client - - - - data['valid_until__gt'] = timezone.localtime() - res = cls.objects.filter(**data) - if res: - return res.last() - - - @classmethod - def get_by_client_id(self, uid): - res = cls.objects.filter(uid=value, - valid_until__gt=timezone.localtime()) - if res: - return self.session_info - - - def set_grant(self, grant): - """ - {'issued_at': 1615403213, 'not_before': 0, 'expires_at': 0, - 'revoked': False, 'used': 0, 'usage_rules': {}, 'scope': [], - 'authorization_details': None, - 'authorization_request': , - 'authentication_event': , - 'claims': {}, 'resources': [], 'issued_token': [], - 'id': 'c695a5e881d311eb905343ee297b1c98', - 'sub': '204176ab8fe8917ee4788683bcee4ebc04bfe1ab659485ec61b2b2b4108c5272', - 'token_map': { - 'authorization_code': , - 'access_token': , - 'refresh_token': } - } - """ - self.issued_at = timezone.make_aware(timezone.datetime.fromtimestamp(grant.issued_at)) - self.sub = grant.sub - self.grant_id = grant.id - - grant.authorization_request = grant.authorization_request.to_json() - grant.authentication_event = grant.authentication_event.to_json() - # breakpoint() - # grant.token_map['authorization_code'] = grant.token_map['authorization_code'].to_json() - # grant.token_map['access_token'] = grant.token_map['access_token'].to_json() - # grant.token_map['refresh_token'] = grant.token_map['refresh_token'].to_json() - grant.token_map.pop('authorization_code') - grant.token_map.pop('access_token') - grant.token_map.pop('refresh_token') - - self.grant = grant.to_json() - self.save() - return grant - - @classmethod - def get_by_session_id(cls, user_uid, client_id, grant_id): - grant = cls.objects.filter(user_uid = user_uid, - client__client_id = client_id, - grant_id = grant_id) - if grant: - return grant.last() - - - - def copy(self): - return dict(sid=self.sid or [], - state=self.state or '', - session_info=self.session_info) - - - def append(self, value): - """Not used, only back compatibility - """ - - - def __iter__(self): - for i in (self.sid,): - yield i - - - def __str__(self): - return 'state: {}'.format(self.state or '') diff --git a/example/django_op/oidc_provider/templates/check_session_iframe.html b/example/django_op/oidc_provider/templates/check_session_iframe.html deleted file mode 100644 index fcd49607..00000000 --- a/example/django_op/oidc_provider/templates/check_session_iframe.html +++ /dev/null @@ -1,122 +0,0 @@ - - - - - Session Management - OP iframe - - - - - - - - - diff --git a/example/django_op/oidc_provider/templates/frontchannel_logout.html b/example/django_op/oidc_provider/templates/frontchannel_logout.html deleted file mode 100644 index 0cca93c1..00000000 --- a/example/django_op/oidc_provider/templates/frontchannel_logout.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - Logout - - - - - - - {{ frames|safe }} - - \ No newline at end of file diff --git a/example/django_op/oidc_provider/templates/logout.html b/example/django_op/oidc_provider/templates/logout.html deleted file mode 100644 index 5b8e91ea..00000000 --- a/example/django_op/oidc_provider/templates/logout.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - Logout Request - - - - - -
-

Do you want to sign-out from {{ op }}?

- -
-
- - -
- - \ No newline at end of file diff --git a/example/django_op/oidc_provider/templates/oidc_login.html b/example/django_op/oidc_provider/templates/oidc_login.html deleted file mode 100644 index 9add475c..00000000 --- a/example/django_op/oidc_provider/templates/oidc_login.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Please login - - - -

{{ page_header }}

- -
- - -

- - -

- -

- - -

- -

- {{ logo_label }} -

-

- {{ tos_label }} -

-

- {{ policy_label }} -

- - -
- - diff --git a/example/django_op/oidc_provider/templates/post_logout.html b/example/django_op/oidc_provider/templates/post_logout.html deleted file mode 100644 index 509c1f7a..00000000 --- a/example/django_op/oidc_provider/templates/post_logout.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Post Logout - - -

You have now been logged out from this server!

- - diff --git a/example/django_op/oidc_provider/templates/user_pass.jinja2 b/example/django_op/oidc_provider/templates/user_pass.jinja2 deleted file mode 100644 index 9add475c..00000000 --- a/example/django_op/oidc_provider/templates/user_pass.jinja2 +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Please login - - - -

{{ page_header }}

- -
- - -

- - -

- -

- - -

- -

- {{ logo_label }} -

-

- {{ tos_label }} -

-

- {{ policy_label }} -

- - -
- - diff --git a/example/django_op/oidc_provider/tests.py b/example/django_op/oidc_provider/tests.py deleted file mode 100644 index 49290204..00000000 --- a/example/django_op/oidc_provider/tests.py +++ /dev/null @@ -1,2 +0,0 @@ - -# Create your tests here. diff --git a/example/django_op/oidc_provider/tests/01_client_db.py b/example/django_op/oidc_provider/tests/01_client_db.py deleted file mode 100644 index 3653f310..00000000 --- a/example/django_op/oidc_provider/tests/01_client_db.py +++ /dev/null @@ -1,102 +0,0 @@ -import datetime -import logging -import json -import random -import string -import pytz - -from django.test import TestCase -from oidc_provider.db_interfaces import OidcClientDatabase -from oidc_provider.models import TIMESTAMP_FIELDS, OidcRelyingParty - - -logger = logging.getLogger('django_test') - - -def randomString(stringLength=10): - """Generate a random string of fixed length """ - letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(stringLength)) - - -CLIENT_ID = randomString() -CLIENT_TEST = {'client_id': '{}'.format(CLIENT_ID), - 'client_salt': '6flfsj0Z', - 'registration_access_token': 'z3PCMmC1HZ1QmXeXGOQMJpWQNQynM4xY', - 'registration_client_uri': 'https://127.0.0.1:8000/registration_api?client_id={}'.format(CLIENT_ID), - 'client_id_issued_at': 1575460012, - 'client_secret': '19cc69b70d0108f630e52f72f7a3bd37ba4e11678ad1a7434e9818e1', - 'client_secret_expires_at': 1575892012, - 'application_type': 'web', - 'contacts': ['ops@example.com'], - 'token_endpoint_auth_method': 'client_secret_basic', - 'jwks_uri': 'https://127.0.0.1:8099/static/jwks.json', - 'redirect_uris': [('https://127.0.0.1:8099/authz_cb/django_oidc_op', {})], - 'post_logout_redirect_uris': [('https://127.0.0.1:8099', None)], - 'response_types': ['code'], - 'grant_types': ['authorization_code'] - } - - -class TestRP(TestCase): - rp = randomString().upper() - cdb = OidcClientDatabase() - now = pytz.utc.localize(datetime.datetime.utcnow()) - - def setUp(self): - self.client_obj = OidcRelyingParty.objects.create(client_id=self.rp, - client_secret_expires_at=self.now, - client_id_issued_at=self.now, - is_active=True) - self.client = self.cdb[self.rp] - print('Created and fetched RP: {}'.format(self.client)) - - def test_get_set_client_db(self): - for key in TIMESTAMP_FIELDS: - value = self.client[key] - assert isinstance(value, int) or \ - isinstance(value, float) - - # test_set_timestamp - for key in TIMESTAMP_FIELDS: - dt_value = self.now+datetime.timedelta(minutes=-60) - self.client[key] = datetime.datetime.timestamp(dt_value) - assert isinstance(self.client[key], int) or \ - isinstance(self.client[key], float) - - # contacts - vt = ['testami@ora.it'] - self.client['contacts'] = vt - assert self.client['contacts'] == vt - # self.client_obj.contacts == vt - - # grant_types - vt = ['authorization_code'] - self.client['grant_types'] = vt - assert self.client['grant_types'] == vt - # self.client_obj.grant_type == vt - - # response_types - vt = ['code'] - self.client['response_types'] = vt - assert self.client['response_types'] == vt - # self.client_obj.response_types == vt - - # post_logout_redirect_uris - vt = [('https://127.0.0.1:8099', None)] - self.client['post_logout_redirect_uris'] = vt - assert self.client['post_logout_redirect_uris'] == vt - # self.client_obj.post_logout_redirect_uris == vt - - # redirect_uris - vt = [('https://127.0.0.1:8099/authz_cb/django_oidc_op', {})] - self.client['redirect_uris'] = vt - assert self.client['redirect_uris'] == vt - # self.client_obj.redirect_uris == vt - - logger.info(json.dumps(self.client.copy(), indent=2)) - - def test_create_as_dict(self): - logger.info('Test creare ad Dict') - self.cdb[CLIENT_ID] = CLIENT_TEST - logger.info(json.dumps(self.cdb[CLIENT_ID], indent=2)) diff --git a/example/django_op/oidc_provider/tests/__init__.py b/example/django_op/oidc_provider/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/example/django_op/oidc_provider/urls.py b/example/django_op/oidc_provider/urls.py deleted file mode 100644 index 5d58e825..00000000 --- a/example/django_op/oidc_provider/urls.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'oidc_provider' - - -urlpatterns = [ - path('.well-known/', views.well_known, - name="oidc_op_well_known"), - path('registration', views.registration, - name="oidc_op_registration"), - path('registration_api', views.registration_api, - name="oidc_op_registration_api"), - - path('authorization', views.authorization, - name="oidc_op_authorization"), - - path('verify/oidc_user_login/', views.verify_user, - name="oidc_op_verify_user"), - path('token', views.token, name="oidc_op_token"), - path('userinfo', views.userinfo, name="oidc_op_userinfo"), - - - path('check_session_iframe', views.check_session_iframe, - name="oidc_op_check_session_iframe"), - path('session', views.session_endpoint, name="oidc_op_session"), - # logout - path('verify_logout', views.verify_logout, - name="oidc_op_verify_logout"), - path('post_logout', views.post_logout, name="oidc_op_post_logout"), - path('rp_logout', views.rp_logout, name="oidc_op_rp_logout"), -] diff --git a/example/django_op/oidc_provider/users.py b/example/django_op/oidc_provider/users.py deleted file mode 100644 index 65c584bb..00000000 --- a/example/django_op/oidc_provider/users.py +++ /dev/null @@ -1,139 +0,0 @@ -import copy - -from django.contrib.auth import authenticate, get_user_model -from django.contrib.auth import get_user_model -from django.template.loader import render_to_string - -from oidcop.user_authn.user import (create_signed_jwt, LABELS) -from oidcop.user_authn.user import UserAuthnMethod - - -class UserPassDjango(UserAuthnMethod): - """ - see oidcop.authn_context - oidcop.endpoint_context - https://docs.djangoproject.com/en/2.2/ref/templates/api/#rendering-a-context - """ - - # TODO: get this though settings conf - url_endpoint = "/verify/user_pass_django" - - def __init__(self, - # template_handler=render_to_string, - template="oidc_login.html", - server_get=None, verify_endpoint='', **kwargs): - """ - template_handler is only for backwards compatibility - it will be always replaced by Django's default - """ - super(UserPassDjango, self).__init__(server_get=server_get) - - self.kwargs = kwargs - self.kwargs.setdefault("page_header", "Log in") - self.kwargs.setdefault("user_label", "Username") - self.kwargs.setdefault("passwd_label", "Password") - self.kwargs.setdefault("submit_btn", "Log in") - self.kwargs.setdefault("tos_uri", "") - self.kwargs.setdefault("logo_uri", "") - self.kwargs.setdefault("policy_uri", "") - self.kwargs.setdefault("tos_label", "") - self.kwargs.setdefault("logo_label", "") - self.kwargs.setdefault("policy_label", "") - - # TODO this could be taken from args - self.template_handler = render_to_string - self.template = template - - self.action = verify_endpoint or self.url_endpoint - self.kwargs['action'] = self.action - - def __call__(self, **kwargs): - _ec = self.server_get('endpoint_context') - # Stores information need afterwards in a signed JWT that then - # appears as a hidden input in the form - jws = create_signed_jwt(_ec.issuer, _ec.keyjar, **kwargs) - - self.kwargs['token'] = jws - - _kwargs = self.kwargs.copy() - for attr in ['policy', 'tos', 'logo']: - _uri = '{}_uri'.format(attr) - _kwargs[_uri] = kwargs.get(_uri) - _label = '{}_label'.format(attr) - _kwargs[_label] = LABELS[_uri] - - return self.template_handler(self.template, _kwargs) - - def verify(self, *args, **kwargs): - username = kwargs["username"] - password = kwargs["password"] - - user = authenticate(username=username, - password=password) - - if username: - return user - else: - raise FailedAuthentication() - - -class UserInfo(object): - """ Read only interface to a user info store """ - - def __init__(self, *args, **kwargs): - self.claims_map = kwargs.get('claims_map', {}) - - def filter(self, user, user_info_claims=None): - """ - Return only those claims that are asked for. - It's a best effort task; if essential claims are not present - no error is flagged. - - :param userinfo: A dictionary containing the available info for one user - :param user_info_claims: A dictionary specifying the asked for claims - :return: A dictionary of filtered claims. - """ - result = {} - if not user.is_active: - return result - - if user_info_claims is None: - return copy.copy(user.__dict__) - else: - missing = [] - optional = [] - for key, restr in user_info_claims.items(): - if key in self.claims_map: - # manage required and optional: TODO extends this approach - if not hasattr(user, self.claims_map.get(key)) and \ - restr == {"essential": True}: - missing.append(key) - continue - else: - optional.append(key) - # - uattr = getattr(user, self.claims_map[key], None) - if not uattr: - continue - result[key] = uattr() if callable(uattr) else uattr - return result - - def __call__(self, user_id, client_id, - user_info_claims=None, **kwargs): - """ - user_id = username - client_id = client id, ex: 'mHwpZsDeWo5g' - """ - user = get_user_model().objects.filter(username=user_id).first() - if not user: - # Todo: raise exception here, this wouldn't be possible. - return {} - - return self.filter(user, user_info_claims) - - def search(self, **kwargs): - for uid, args in self.db.items(): - if dict_subset(kwargs, args): - return uid - - raise KeyError('No matching user') diff --git a/example/django_op/oidc_provider/utils.py b/example/django_op/oidc_provider/utils.py deleted file mode 100644 index 1700178d..00000000 --- a/example/django_op/oidc_provider/utils.py +++ /dev/null @@ -1,31 +0,0 @@ -import datetime -import json -import pytz - -from oidcmsg.message import Message -from cryptojwt.key_jar import KeyJar - -from . views import oidcop_app - - -def timestamp2dt(value): - return int(datetime.datetime.timestamp(value)) - - -def dt2timestamp(value): - pytz.utc.localize(datetime.datetime.fromtimestamp(value)) - - -def decode_token(txt, attr_name='access_token', verify_sign=True): - issuer = oidcop_app.srv_config.conf['op']['server_info']['issuer'] - jwks_path = oidcop_app.srv_config.conf['OIDC_KEYS']['private_path'] - jwks = json.loads(open(jwks_path).read()) - - key_jar = KeyJar() - key_jar.import_jwks(jwks, issuer=issuer) - - jwt = json.loads(txt) - msg = Message().from_jwt(jwt.get(attr_name, ''), - keyjar=key_jar, - verify=verify_sign) - return msg diff --git a/example/django_op/oidc_provider/views.py b/example/django_op/oidc_provider/views.py deleted file mode 100644 index 89b7f023..00000000 --- a/example/django_op/oidc_provider/views.py +++ /dev/null @@ -1,351 +0,0 @@ -import base64 -import logging -import json -import os -import urllib - -from django.conf import settings -from django.http import (HttpResponse, - HttpResponseBadRequest, - HttpResponseRedirect, - JsonResponse) -from django.http.request import QueryDict -from django.views.decorators.csrf import csrf_exempt -from django.shortcuts import render -from oidcop.authn_event import create_authn_event -from oidcop.exception import UnAuthorizedClient -from oidcop.exception import UnAuthorizedClientScope # experimental -from oidcop.exception import InvalidClient -from oidcop.exception import UnknownClient -from oidcop.session.token import AccessToken -from oidcop.oidc.token import Token -from oidcmsg.oauth2 import ResponseMessage -from oidcmsg.oidc import AccessTokenRequest -from oidcmsg.oidc import AuthorizationRequest -from urllib import parse as urlib_parse -from urllib.parse import urlparse - - -from . application import oidcop_application -oidcop_app = oidcop_application() - -logger = logging.getLogger(__name__) -IGNORE = ["cookie", "user-agent"] - - -def _add_cookie(resp, cookie_spec): - kwargs = {'value': cookie_spec["value"]} - for param in ['expires', 'max-age']: - if param in cookie_spec: - kwargs[param] = cookie_spec[param] - kwargs["path"] = "/" - resp.set_cookie(cookie_spec["name"], **kwargs) - - -def add_cookie(resp, cookie_spec): - if isinstance(cookie_spec, list): - for _spec in cookie_spec: - _add_cookie(resp, _spec) - elif isinstance(cookie_spec, dict): - _add_cookie(resp, cookie_spec) - - -def do_response(endpoint, req_args, error='', **args): - info = endpoint.do_response(request=req_args, error=error, **args) - _response_placement = info.get('response_placement') - if not _response_placement: - _response_placement = endpoint.response_placement - - info_response = info['response'] - # Debugging things - try: - response_params = json.dumps(json.loads(info_response), indent=2) - logger.debug('Response params: {}\n'.format(response_params)) - except: - url, args = urllib.parse.splitquery(info_response) - response_params = urllib.parse.parse_qs(args) - resp = json.dumps(response_params, indent=2) - logger.debug('Response params: {}\n{}\n\n'.format(url, resp)) - # end debugging - - if error: - if _response_placement == 'body': - logger.debug('Error Response [Body]: {}'.format(info_response)) - resp = HttpResponse(info_response, status=400) - else: # _response_placement == 'url': - logger.debug('Redirect to: {}'.format(info_response)) - resp = HttpResponseRedirect(info_response) - else: - if _response_placement == 'body': - #logger.debug('Response [Body]: {}'.format(info_response)) - resp = HttpResponse(info_response, status=200) - else: # _response_placement == 'url': - #logger.debug('Redirect to: {}'.format(info_response)) - resp = HttpResponseRedirect(info_response) - - for key, value in info['http_headers']: - # set response headers - resp[key] = value - - if 'cookie' in info: - add_cookie(resp, info['cookie']) - - return resp - - -def fancy_debug(request): - """ - fancy logging of JWT things - """ - _headers = json.dumps(dict(request.headers), indent=2) - logger.debug('Request Headers: {}\n\n'.format(_headers)) - - _get = json.dumps(dict(request.GET), indent=2) - if request.GET: - logger.debug('Request arguments GET: {}\n'.format(_get)) - if request.POST or request.body: - _post = request.POST or request.body - if isinstance(_post, bytes): - _post = json.dumps(json.loads(_post.decode()), indent=2) - elif isinstance(_post, QueryDict): - _post = json.dumps({k: v for k, v in _post.items()}, - indent=2) - logger.debug('Request arguments POST: {}\n'.format(_post)) - - -def service_endpoint(request, endpoint): - """ - TODO: documentation here - """ - logger.info('\n\nRequest at the "{}" endpoint'.format(endpoint.name)) - # if logger.level == 0: - # fancy_debug(request) - - http_info = { - "headers": {k.lower(): v for k, v in request.headers.items() if k not in IGNORE}, - "method": request.method, - "url": request.get_raw_uri(), - # name is not unique - "cookie": [{"name": k, "value": v} for k, v in request.COOKIES.items()] - } - - if request.method == 'GET': - data = {k: v for k, v in request.GET.items()} - elif request.body: - data = request.body \ - if isinstance(request.body, str) else \ - request.body.decode() - # - if 'authorization' in http_info.get('headers', ()): - data = {k: v[0] for k, v in urlib_parse.parse_qs(data).items()} - else: - data = {k: v for k, v in request.POST.items()} - - req_args = endpoint.parse_request(data, http_info=http_info) - - 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 (InvalidClient, UnknownClient, UnAuthorizedClient) as err: - logger.error(err) - return JsonResponse(json.dumps({ - 'error': 'unauthorized_client', - 'error_description': str(err) - }), safe=False, status=400) - except Exception as err: - logger.error(err) - return JsonResponse(json.dumps({ - 'error': 'invalid_request', - 'error_description': str(err), - 'method': request.method - }), safe=False, status=400) - - if isinstance(req_args, ResponseMessage) and 'error' in req_args: - return JsonResponse(req_args.__dict__, status=400) - - if 'redirect_location' in args: - return HttpResponseRedirect(args['redirect_location']) - if 'http_response' in args: - return HttpResponse(args['http_response'], status=200) - - return do_response(endpoint, req_args, **args) - - -def well_known(request, service): - """ - /.well-known/ - """ - if service == 'openid-configuration': - _endpoint = oidcop_app.endpoint_context.endpoint['provider_config'] - # TODO fedservice integration here - # if service == 'openid-federation': - # _endpoint = oidcop_app.endpoint_context.endpoint['provider_info'] - elif service == 'webfinger': - _endpoint = oidcop_app.endpoint_context.endpoint['webfinger'] - else: - return HttpResponseBadRequest('Not supported', status=400) - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def registration(request): - logger.info('registration request') - _endpoint = oidcop_app.endpoint_context.endpoint['registration'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def registration_api(): - logger.info('registration API') - return service_endpoint( - request, - oidcop_app.endpoint_context.endpoint['registration_api'] - ) - - -def authorization(request): - _endpoint = oidcop_app.endpoint_context.endpoint['authorization'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def verify_user(request): - """csrf is not needed because it uses oidc token in the post - """ - token = request.POST.get('token') - if not token: - return HttpResponse('Access forbidden: invalid token.', status=403) - authn_method = oidcop_app.endpoint_context.endpoint_context.authn_broker.get_method_by_id('user') - - kwargs = dict([(k, v) for k, v in request.POST.items()]) - user = authn_method.verify(**kwargs) - if not user: - return HttpResponse('Authentication failed', status=403) - - auth_args = authn_method.unpack_token(kwargs['token']) - authz_request = AuthorizationRequest().from_urlencoded(auth_args['query']) - - # salt size can be customized in settings.OIDC_OP_AUTHN_SALT_SIZE - salt_size = getattr(settings, 'OIDC_OP_AUTHN_SALT_SIZE', 4) - authn_event = create_authn_event( - uid=user.username, - salt=base64.b64encode(os.urandom(salt_size)).decode(), - authn_info=auth_args['authn_class_ref'], - authn_time=auth_args['iat'] - ) - - endpoint = oidcop_app.endpoint_context.endpoint['authorization'] - # cinfo = endpoint.endpoint_context.cdb[authz_request["client_id"]] - - # {'session_id': 'diana;;client_3;;38044288819611eb905343ee297b1c98', 'identity': {'uid': 'diana'}, 'user': 'diana'} - client_id = authz_request["client_id"] - _token_usage_rules = endpoint.server_get("endpoint_context").authn_broker.get_method_by_id('user') - - session_manager = oidcop_app.endpoint_context.endpoint_context.session_manager - _session_id = session_manager.create_session( - authn_event=authn_event, - auth_req=authz_request, - user_id=user.username, - client_id=client_id, - token_usage_rules=_token_usage_rules - ) - - try: - args = endpoint.authz_part2(user=user.username, - session_id = _session_id, - request=authz_request, - authn_event=authn_event) - except ValueError as excp: - msg = 'Something wrong with your Session ... {}'.format(excp) - return HttpResponse(msg, status=403) - - if isinstance(args, ResponseMessage) and 'error' in args: - return HttpResponse(args.to_json(), status=400) - - response = do_response(endpoint, request, **args) - return response - - -@csrf_exempt -def token(request): - logger.info('token request') - _endpoint = oidcop_app.endpoint_context.endpoint['token'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def userinfo(request): - logger.info('userinfo request') - _endpoint = oidcop_app.endpoint_context.endpoint['userinfo'] - return service_endpoint(request, _endpoint) - - -######## -# LOGOUT -######## -def session_endpoint(request): - return service_endpoint( - request, - oidcop_app.endpoint_context.endpoint['session'] - ) - - -def check_session_iframe(request): - if request.method == 'GET': - req_args = request.GET - elif request.method == 'POST': - req_args = json.loads(request.POST) - else: - req_args = dict([(k, v) for k, v in request.body.items()]) - - if req_args: - # will contain client_id and origin - if req_args['origin'] != oidcop_app.endpoint_context.conf['issuer']: - return 'error' - if req_args['client_id'] != current_app.endpoint_context.cdb: - return 'error' - return 'OK' - - logger.debug('check_session_iframe: {}'.format(req_args)) - res = render(request, template_name='check_session_iframe.html') - return res - - -@csrf_exempt -def rp_logout(request): - _endp = oidcop_app.endpoint_context.endpoint['session'] - _info = _endp.unpack_signed_jwt(request.POST['sjwt']) - alla = None # request.POST.get('logout') - - _iframes = _endp.do_verified_logout(alla=alla, **_info) - if _iframes: - d = dict(frames=" ".join(_iframes), - size=len(_iframes), - timeout=5000, - postLogoutRedirectUri=_info['redirect_uri']) - res = render(request, 'frontchannel_logout.html', d) - - else: - res = HttpResponseRedirect(_info['redirect_uri']) - try: - _endp.kill_cookies() - except AttributeError: - logger.debug('Cookie not implemented or not working.') - #_add_cookie(res, _kakor) - return res - - -def verify_logout(request): - part = urlparse(oidcop_app.endpoint_context.conf['issuer']) - d = dict(op=part.hostname, - do_logout='rp_logout', - sjwt=request.GET['sjwt'] or request.POST['sjwt']) - return render(request, 'logout.html', d) - - -def post_logout(request): - return render(request, 'post_logout.html') diff --git a/example/django_op/requirements.txt b/example/django_op/requirements.txt deleted file mode 100644 index 263f1c08..00000000 --- a/example/django_op/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -Django>3.1,<4.0 -oidcop -gunicorn -filelock - -git+https://github.com/IdentityPython/JWTConnect-Python-CryptoJWT -git+https://github.com/IdentityPython/JWTConnect-Python-OidcMsg -git+https://github.com/IdentityPython/JWTConnect-Python-OidcService -git+https://github.com/IdentityPython/oidcendpoint.git -git+https://github.com/IdentityPython/JWTConnect-Python-OidcRP diff --git a/example/django_op/snippets/db_interfaces.py b/example/django_op/snippets/db_interfaces.py deleted file mode 100644 index 7c41d478..00000000 --- a/example/django_op/snippets/db_interfaces.py +++ /dev/null @@ -1,287 +0,0 @@ -import datetime -import json -import logging -import pytz - -from django.contrib.auth import get_user_model -from django.utils import timezone -from oidcop.session.database import Database -from oidcop.session.info import UserSessionInfo -from . models import (OidcRelyingParty, - OidcRPResponseType, - OidcRPGrantType, - OidcRPContact, - OidcRPRedirectUri, - OidcSession, - OidcSessionSso, - TIMESTAMP_FIELDS, - is_state, - is_sid, - is_sub, - is_code) - - -logger = logging.getLogger(__name__) - - -class OidcClientDb(object): - """ - Adaptation of a Django model as if it were a dict - """ - model = OidcRelyingParty - - def __init__(self, *args, **kwargs): - pass - - def __contains__(self, key): - if self.model.objects.filter(client_id=key).first(): - return 1 - - def __iter__(self): - values = self.model.objects.all().values_list('client_id') - self.clients = [cid[0] for cid in values] - for value in (self.clients): - yield value - - def get(self, key, excp=None, as_obj=False): - client = self.model.objects.filter(client_id=key, - is_active=True).first() - if not client: - return excp - - # set last_seen - client.last_seen = timezone.localtime() - client.save() - if as_obj: - return client - return client.copy() - - def __getitem__(self, key): - value = self.get(key) - if not value: - raise KeyError - return value - - def keys(self): - return self.model.objects.values_list('client_id', flat=True) - - def __setitem__(self, key, value): - return self.set(key, value) - - def set(self, key, value): - dv = value.copy() - - for k, v in dv.items(): - if isinstance(v, int) or isinstance(v, float): - if k in TIMESTAMP_FIELDS: - dt = datetime.datetime.fromtimestamp(v) - dv[k] = pytz.utc.localize(dt) - - client = None - # if the client already exists - if dv.get('id'): - client = self.model.objects.\ - filter(pk=dv['id']).first() - - if dv.get('client_id'): - client = self.model.objects.\ - filter(client_id=dv['client_id']).first() - - if not client: - client_id = dv.pop('client_id') - client = self.model.objects.create(client_id=client_id) - - for k, v in dv.items(): - setattr(client, k, v) - - client.save() - - def __str__(self): - return self.__dict__ - - -class OidcSessionDb(Database): - """ - Adaptation of a Django model as if it were a dict - - This class acts like a NoSQL storage but stores informations - into a pure Django DB model - """ - - def __init__(self, conf_db=None, session_db=None, sso_db=None, cdb=None): - self.conf_db = conf_db - self.db = session_db or OidcSession - self.sso_db = sso_db or OidcSessionSso - self.cdb = cdb or OidcClientDb() - - def get_valid_sessions(self): - return self.db.objects.filter().exclude(valid_until__lte=timezone.localtime()) - - def get_by_sid(self, value): - session = self.db.get_by_sid(value) - if session: - return session - - def get_by_state(self, value): - session = self.get_valid_sessions().filter(state=value) - if session: - return session.last() - - def create_by_state(self, state): - return self.db.objects.create(state=state) - - def __iter__(self): - self.elems = self.keys() - for value in (self.elems): - yield value - - def __getitem__(self, *args, **kwargs): - return self.get(*args, **kwargs) - - def get(self, key, excp=None): - if is_sid(key): - elem = self.db.get_by_sid(key) - elif is_code(key): - elem = self.get_valid_sessions().filter(code=key).last() - else: - # state is unpredictable, it's client side. - # elem = self.get_valid_sessions().filter(state=key).last() - elem = self.get_valid_sessions().filter(uid=key).last() - - if not elem: - return - elif elem.sid and elem.sid == key: - return json.loads(elem.session_info) - # elif elem.state == key: - elif elem.uid == key: - return elem.sso.sid - - def set_session_info(self, info_dict): - # info_dict = {'user_id': 'wert', 'subordinate': [], 'revoked': False, 'type': 'UserSessionInfo'} - - session = self.get_valid_sessions().get( - state=info_dict['authn_req']['state']) - session.session_info = json.dumps(info_dict) - session.code = info_dict.get('code') - authn_event = info_dict.get('authn_event') - valid_until = authn_event.get('valid_until') - if valid_until: - dt = datetime.datetime.fromtimestamp(valid_until) - session.valid_until = pytz.utc.localize(dt) - - client_id = info_dict.get('client_id') - session.client = self.cdb.get(key=client_id, as_obj=True) - session.save() - - def set(self, key, value): - if is_sid(key): - # info_dict = {'code': 'Z0FBQUFBQmZESFowazFBWWJteTNMOTZQa25KZmV0N1U1VzB4VEZCVEN3SThQVnVFRWlSQ2FrODhpb3Yyd3JMenJQT01QWGpuMnJZQmQ4YVh3bF9sbUxqMU43VG1RQ01BbW9JdV8tbTNNSzREMUk2U2N4YXVwZ3ZWQ1ZvbXdFanRsbWJIaWQyVWZON0N5LU9mUlhZUGgwdFRDQkpRZ3dSR0lVQjBBT0s4OHc3REJOdUlPUGVOUU9ZRlZvU3FBdVU2LThUUWNhRDVocl9QWEswMmo3Y2VtLUNvWklsX0ViN1NfWFRJWksxSXhxNVVNQW9ySngtc2RCST0=', 'oauth_state': 'authz', 'client_id': 'Mz2LUfvqCbRQ', 'authn_req': {'redirect_uri': 'https://127.0.0.1:8099/authz_cb/django_oidc_op', 'scope': 'openid profile email address phone', 'response_type': 'code', 'nonce': 'mpuLL5IxgDvFDGAqlE05LwHO', 'state': 'eOzFkkGFHLT16zO6SqpOmc2rv6DZmf3g', 'code_challenge': 'lAs7I04g1Qh8mhTG8wxV0BfmrhzrSrl1ASp04C3Zmog', 'code_challenge_method': 'S256', 'client_id': 'Mz2LUfvqCbRQ'}, 'authn_event': {'uid': 'wert', 'salt': 'fc7AGQ==', 'authn_info': 'oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD', 'authn_time': 1594652276, 'valid_until': 1594655876}} - info_dict = value - self.set_session_info(info_dict) - logger.debug('Session DB - set - {}'.format(session.copy())) - - def __setitem__(self, sid, instance): - if is_sid(sid): - try: - instance.to_json() - except ValueError: - json.dumps(instance) - except AttributeError: - # it's a dict - pass - - self.set_session_info(instance) - - elif isinstance(instance, UserSessionInfo): - # {'_dict': {'user_id': 'wert', 'subordinate': [], 'revoked': False, 'type': 'UserSessionInfo'}, 'lax': False, 'jwt': None, 'jws_header': None, 'jwe_header': None, 'verify_ssl': True} - self.set_session_info(instance.__dict__['_dict']) - - else: - logger.error('{} tries __setitem__ {} in {}'.format(sid, - instance, - self.__class__.__name__)) - - def __delitem__(self, key): - if is_sid(key): - ses = self.db.get_by_sid(key) - if ses: - ses.sso.delete() - ses.delete() - - -class OidcSsoDb(object): - """ - Adaptation of a Django model as if it were a dict - - This class acts like a NoSQL storage but stores informations - into a pure Django DB model - """ - - def __init__(self, db_conf={}, db=None, session_handler=None): - self._db = db or OidcSessionSso - self._db_conf = db_conf - self.session_handler = session_handler or db_conf.get( - 'session_hanlder') or OidcSessionDb() - - def _get_or_create(self, sid): - sso = self._db.objects.filter(sid=sid).first() - if not sso: - sso = self._db.objects.create(sid=sid) - return sso - - def __setitem__(self, k, value): - if isinstance(value, dict): - if value.get('state'): - session = self.session_handler.create_by_state(k) - session.sid = value['state'][0] \ - if isinstance(value['state'], list) else value - sso = self._db.objects.create() - session.sso = sso - session.save() - else: - # it would be quite useless for this implementation ... - # k = '81c58c4037ab1939423ab4fb8b472fdd5fc3a3939e4debc81f52ed37' - # value = - pass - - - def set(self, k, v): - logging.info('{}:{} - already there'.format(k, v)) - - - def get(self, k, default): - session = self.session_handler.get_by_state(k) - if session: - return session - - if is_sub(k): - # sub - return self._db.objects.filter(sub=k).last() or {} - elif is_sid(k): - # sid - session = self.session_handler.get_by_sid(k) - return session.sso if session else {} - else: - logger.debug(("{} can't find any attribute " - "with this name as attribute: {}").format(self, k)) - user = get_user_model().objects.filter(username=k).first() - if user: - logger.debug( - 'Tryng to match to a username: Found {}'.format(user)) - return self._db.objects.filter(user=user).last() - else: - return {} - - def __delitem__(self, name): - self.delete(name) - - def delete(self, name): - session = self.session_handler.get_by_state(name) - - if is_sid(name): - session = self.session_handler.get_by_sid(name) - elif is_sub(name): - sso = self._db.objects.filter(sub=name) - sso.delete() - if session: - session.delete() diff --git a/example/django_op/snippets/msg_test.py b/example/django_op/snippets/msg_test.py deleted file mode 100644 index 922e571d..00000000 --- a/example/django_op/snippets/msg_test.py +++ /dev/null @@ -1,39 +0,0 @@ -import json -import requests - -from cryptojwt.key_jar import KeyJar -from oidcmsg.message import Message - - -issuer = "https://127.0.0.1:8000" -issuer_metadata_url = '{}/.well-known/openid-configuration'.format(issuer) -issuer_metadata = requests.get(issuer_metadata_url, verify=False).text -issuer_jwks_uri = json.loads(issuer_metadata)['jwks_uri'] - -jwks_raw = requests.get(issuer_jwks_uri, verify=False).text -jwks = json.loads(jwks_raw) - - -# built keyjar -key_jar = KeyJar() -key_jar.import_jwks(jwks, issuer=issuer) - -# it depends by JWT, is the signing key identifier could be extracted from it then it should be verified, otherwise not -# see difference between at and unverifiable_at -verify_sign = 1 - -at = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJXdG9SekV4VXkxak9GVXlSV2hwZUdkbFREWlBaME55TW1ka05ERlFaakJSUzJreVQwaExVazVJUVEifQ.eyJzdWIiOiAiODAzMjcwNDJiOTZiOWYxYzAwZDlkMDRkYjgxNmU4NGFmNGUzNjE2ZGIxZDA2OTRiMTNhYjg2ZjQ5ZmQyNTFiZiIsICJhdXRoX3RpbWUiOiAxNTc2MDU4MTc4LCAiYWNyIjogIm9pZGNlbmRwb2ludC51c2VyX2F1dGhuLmF1dGhuX2NvbnRleHQuSU5URVJORVRQUk9UT0NPTFBBU1NXT1JEIiwgImVtYWlsIjogImdpdXNlcHBlLmRlbWFyY29AdW5pY2FsLml0IiwgIm5vbmNlIjogImFFbERSTUNBUWhHZWVBVUs1b0RyRG1PdSIsICJpc3MiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6ODAwMCIsICJpYXQiOiAxNTc2MDU4MTc4LCAiZXhwIjogMTU3NjA1ODQ3OCwgImF1ZCI6IFsiejl3VU90dG1GM3FjIl19.lAKU1F_77T11gNcDvRMHy3hEJANaCkjTtbyR5D7DmOFOTKTYBWIT_ZqEmGnBmGpDBhnGcwbupFavltAOq03PGyJYu-Bxk3ktqTCjfcLTQroE-o3KTx_xYjVDh60p3RAAK9iD5ligmH2I5vFtFl-5ckTl2tX9Zn_IAqJrpuCIvgwBHzje-61upXKyXzBthOKa2dWAlOMHXYKlFV_4mhnr3Q3RNP8hmgFkltD3d-INo8tlysRQebuxnQ7LizQtOIhWACioHZosfpQIu2_L89PiTdcUuFeAeRNa_kUe9Oztk7ffCcDk2Xqa5C-8gxpgHTX6STEa59jeG07q7_s4pj7Fig" -unverifiable_at = "eyJhbGciOiJFUzI1NiIsImtpZCI6IlQwZGZTM1ZVYUcxS1ZubG9VVTQwUXpJMlMyMHpjSHBRYlMxdGIzZ3hZVWhCYzNGaFZWTlpTbWhMTUEifQ.eyJzaWQiOiAiNzUyYTc2NWZlMjg4MGE4NmU2OTlkNTcxZWM4YTE2MWE2NTkzZjVhMmJlNzdkMDkzZDRmNzU2MmUiLCAidHR5cGUiOiAiVCIsICJzdWIiOiAiODAzMjcwNDJiOTZiOWYxYzAwZDlkMDRkYjgxNmU4NGFmNGUzNjE2ZGIxZDA2OTRiMTNhYjg2ZjQ5ZmQyNTFiZiIsICJlbWFpbCI6ICJnaXVzZXBwZS5kZW1hcmNvQHVuaWNhbC5pdCIsICJnZW5kZXIiOiAibWFsZSIsICJpc3MiOiAiIiwgImlhdCI6IDE1NzYwNTI3ODMsICJleHAiOiAxNTc2MDU2MzgzLCAiYXVkIjogWyJ6OXdVT3R0bUYzcWMiLCAiaHR0cHM6Ly8xMjcuMC4wLjE6ODAwMCJdfQ.xjkeVPF8fcHZVVZa3wYHxatyoVizML10iD579T_HB1tj7Cm_JNRdKB0nUoq7HddfeeNG911YNWHHl0lD9TQ2gA" - -m = Message().from_jwt(at, keyjar=key_jar, verify=verify_sign) - - -print('Access Token header: ', m.jws_header) -print('Access Token payload: ', m) - -t = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJXdG9SekV4VXkxak9GVXlSV2hwZUdkbFREWlBaME55TW1ka05ERlFaakJSUzJreVQwaExVazVJUVEifQ.eyJzdWIiOiAiODAzMjcwNDJiOTZiOWYxYzAwZDlkMDRkYjgxNmU4NGFmNGUzNjE2ZGIxZDA2OTRiMTNhYjg2ZjQ5ZmQyNTFiZiIsICJhdXRoX3RpbWUiOiAxNTc2MDUyNzc3LCAiYWNyIjogIm9pZGNlbmRwb2ludC51c2VyX2F1dGhuLmF1dGhuX2NvbnRleHQuSU5URVJORVRQUk9UT0NPTFBBU1NXT1JEIiwgImVtYWlsIjogImdpdXNlcHBlLmRlbWFyY29AdW5pY2FsLml0IiwgIm5vbmNlIjogIlRKdnFjYW85RjRubkp5bTBPZnlNeXlBbSIsICJpc3MiOiAiaHR0cHM6Ly8xMjcuMC4wLjE6ODAwMCIsICJpYXQiOiAxNTc2MDUyNzgzLCAiZXhwIjogMTU3NjA1MzA4MywgImF1ZCI6IFsiejl3VU90dG1GM3FjIl19.kQq0WBXRTUcKY7RI8A0Yb1CyoOds5-EnDrc7mhc4pzp5xig9lOUWaBts6Q5vwoI29-iadPUf62SIcrBuGPAyvAFHC-pqSy-vcXbYFpWN18n9SaL7O2fkQD-PlPl088X6fJ-Muqt_RvkRjY_cwY-VuJNh3_uXXZdsLnUimsLI3E4q6aCMkoaqDajbq35L40_xcjRhmQHh9Rn_4pIfnRGKZdsQICYJ6ivLSO5dxxydDtyA1nip1ER8hV3ISj8qfJq1_tFe5JrvcEWXbs9MMmsnlXTTB6X-71ysmzqVcjY2o-CviZGQTwEXwJYY54_u24NUwwM72pWVL97N1ug7nIlusA" -m = Message().from_jwt(t, keyjar=key_jar, verify=verify_sign) - - -print('ID Token header: ', m.jws_header) -print('ID Token payload: ', m) diff --git a/example/django_op/snippets/rp_handler.py b/example/django_op/snippets/rp_handler.py deleted file mode 100644 index 27ae4e57..00000000 --- a/example/django_op/snippets/rp_handler.py +++ /dev/null @@ -1,178 +0,0 @@ -import os -import re -import requests -import json -import urllib -import urllib3 - -from cryptojwt import KeyJar -from cryptojwt.key_jar import init_key_jar - -from oidcmsg.message import Message -from oidcrp import rp_handler -from oidcrp.util import load_yaml_config - -RPHandler = rp_handler.RPHandler - -def decode_token(bearer_token, keyjar, verify_sign=True): - msg = Message().from_jwt(bearer_token, - keyjar=keyjar, - verify=verify_sign) - return msg.to_dict() - - -def init_oidc_rp_handler(app): - _rp_conf = app.config - - if _rp_conf.get('rp_keys'): - _kj = init_key_jar(**_rp_conf['rp_keys']) - _path = _rp_conf['rp_keys']['public_path'] - # removes ./ and / from the begin of the string - _path = re.sub('^(.)/', '', _path) - else: - _kj = KeyJar() - _path = '' - _kj.httpc_params = _rp_conf['httpc_params'] - hash_seed = app.config.get('hash_seed', "BabyHoldOn") - rph = RPHandler(_rp_conf['base_url'], _rp_conf['clients'], services=_rp_conf['services'], - hash_seed=hash_seed, keyjar=_kj, jwks_path=_path, - httpc_params=_rp_conf['httpc_params']) #, verify_ssl=False) - - return rph - - -def get_rph(config_fname): - config = load_yaml_config(config_file) - app = type('RPApplication', (object,), {"config": config}) - rph = init_oidc_rp_handler(app) - return rph - - -def fancy_print(msg, dict_obj): - print('\n\n{}\n'.format(msg), - json.dumps(dict_obj, indent=2) if dict_obj else '') - - -def run_rp_session(rph, issuer_id, username, password): - # register client (provider info and client registration) - info = rph.begin(issuer_id) - - issuer_fqdn = rph.hash2issuer[issuer_id] - issuer_keyjar = rph.issuer2rp[issuer_fqdn] - - fancy_print("Client registration done...\n" - "Connecting to Authorization url:", - info) - - # info contains the url to the authorization endpoint - # {'url': 'https://127.0.0.1:8000/authorization?redirect_uri=https%3A%2F%2F127.0.0.1%3A8099%2Fauthz_cb%2Fdjango_oidc_op&scope=openid+profile+email+address+phone&response_type=code&nonce=HhDGhvuIoQ9MaVLSqXi3D6r4&state=AdgqZVTxwdHaE9kjRUCLTnI78GpoQq90&code_challenge=v3UDlTl4qOrbA1owsEBdKMHwSubmvheGrjUiBeCQqhk&code_challenge_method=S256&client_id=shoInN4jcqIe', 'state': 'AdgqZVTxwdHaE9kjRUCLTnI78GpoQq90'} - - print('\nStarting connection to Authorization URL') - res = requests.get(info['url'], verify=rph.verify_ssl) - - # this contains the html form - # res.text - - #'\n\n\n\n \n Please login\n\n\n\n

Testing log in

\n\n
\n \n\n

\n \n \n

\n\n

\n \n \n

\n\n

\n \n

\n

\n \n

\n

\n \n

\n\n \n
\n\n\n' - try: - auth_code = re.search('value="(?P[a-zA-Z\-\.\_0-9]*)"', res.text).groupdict()['token'] - auth_url = re.search('action="(?P[a-zA-Z0-9\/\-\_\.\:]*)"', res.text).groupdict()['url'] - except Exception as e: - print(res.text) - raise Exception(res.text) - - fancy_print("The Authorization endpoint returns a " - "HTML authentication form with a token", - {'token': auth_code, - 'url': auth_url}) - - fancy_print('Authorization code content: ', - decode_token(auth_code, - keyjar=issuer_keyjar.get_service_context().keyjar -, - verify_sign=False)) - - auth_dict = {'username': username, - 'password': password, - 'token': auth_code} - - auth_url = '/'.join((issuer_fqdn, auth_url)) - print('Credential form submit ...') - req = requests.post(auth_url, - data=auth_dict, verify=rph.verify_ssl, - allow_redirects=False) - print('Credential form submitted!') - - # req is a 302, a redirect to the https://127.0.0.1:8099/authz_cb/django_oidc_op - if req.status_code != 302: - raise Exception(req.content) - rp_final_url = req.headers['Location'] - - fancy_print("The Authorization returns a " - "HttpRedirect (302) to {}".format(rp_final_url), None) - - # what we found later in request_args ... - # code = urllib.parse.parse_qs( urllib.parse.urlparse(rp_final_url).query) - # fancy_print("That contains: ", code) - - ws, args = urllib.parse.splitquery(rp_final_url) - request_args = urllib.parse.parse_qs(args) - - # from list to str - request_args = {k:v[0] for k,v in request_args.items()} - - fancy_print("Going to finalize the session ...", request_args) - - # oidcrp.RPHandler.finalize() will parse the authorization response and depending on the configuration run the needed RP service - result = rph.finalize(request_args['iss'], request_args) - - fancy_print("Got Access Token.", None) - - # Tha't the access token, the bearer used to access to userinfo endpoint - # result['token'] - fancy_print("Bearer Access Token", result['token']) - - # get the keyjar related to the issuer - decoded_access_token = decode_token(result['token'], - keyjar=issuer_keyjar.get_service_context().keyjar) - fancy_print("Access Token", decoded_access_token) - - # id_token - # import pdb; pdb.set_trace() - # result['id_token'].to_dict() - fancy_print("ID Token", result['id_token'].to_dict()) - - # userinfo - result['userinfo'].to_dict() - fancy_print("Userinfo endpoint result:", result['userinfo'].to_dict()) - - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument('-conf', required=True, - help="settings file where RP configuration is") - parser.add_argument('-u', required=True, - help="username") - parser.add_argument('-p', required=True, - help="password") - parser.add_argument('-iss', required=True, - help="The issuer Id you want to " - "requests authorization. Example: " - "django_oidc_op") - args = parser.parse_args() - - # 'django-oidc-op/example/data/oidc_rp/conf.django.yaml' - config_file = args.conf - rph = get_rph(config_file) - - rph.verify_ssl = rph.httpc_params['verify'] - if not rph.verify_ssl: - urllib3.disable_warnings() - - # select the OP you want to, example: "django_oidc_op" - issuer_id = args.iss - run_rp_session(rph, issuer_id, args.u, args.p) - -# python3 snippets/rp_handler.py -conf example/data/oidc_rp/conf.django.yaml -u user -p pass -iss django_oidc_op diff --git a/example/flask_op/config.json b/example/flask_op/config.json index 598914f7..e3758929 100644 --- a/example/flask_op/config.json +++ b/example/flask_op/config.json @@ -88,7 +88,7 @@ }, "authentication": { "user": { - "acr": "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD", + "acr": "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword", "class": "oidcop.user_authn.user.UserPassJinja2", "kwargs": { "verify_endpoint": "verify/user", @@ -185,7 +185,10 @@ "class": "oidcop.oauth2.introspection.Introspection", "kwargs": { "client_authn_method": [ - "client_secret_post" + "client_secret_post", + "client_secret_basic", + "client_secret_jwt", + "private_key_jwt" ], "release": [ "username" @@ -285,7 +288,7 @@ "kwargs": { "scheme_map": { "email": [ - "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD" + "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword" ] } } diff --git a/example/flask_op/config.yaml b/example/flask_op/config.yaml index edc66b96..f7e7c7e1 100644 --- a/example/flask_op/config.yaml +++ b/example/flask_op/config.yaml @@ -191,7 +191,7 @@ op: db_file: users.json authentication: user: - acr: oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD + acr: urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword class: oidcop.user_authn.user.UserPassJinja2 kwargs: verify_endpoint: 'verify/user' @@ -235,7 +235,7 @@ op: kwargs: scheme_map: email: - - oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD + - urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword # this adds PKCE support as mandatory - disable it if needed (essential: False) add_on: diff --git a/example/flask_op/views.py b/example/flask_op/views.py index 7d8f913c..06a7f180 100644 --- a/example/flask_op/views.py +++ b/example/flask_op/views.py @@ -183,6 +183,10 @@ def token(): return service_endpoint( current_app.server.server_get("endpoint", 'token')) +@oidc_op_views.route('/introspection', methods=['POST']) +def introspection_endpoint(): + return service_endpoint( + current_app.server.server_get("endpoint", 'introspection')) @oidc_op_views.route('/userinfo', methods=['GET', 'POST']) def userinfo(): @@ -244,7 +248,6 @@ def service_endpoint(endpoint): _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) diff --git a/src/oidcop/authn_event.py b/src/oidcop/authn_event.py index 98d90814..0b5bf0e8 100644 --- a/src/oidcop/authn_event.py +++ b/src/oidcop/authn_event.py @@ -47,7 +47,6 @@ def create_authn_event( :param kwargs: :return: """ - args = {"uid": uid, "authn_info": authn_info} if sub: diff --git a/src/oidcop/authz/__init__.py b/src/oidcop/authz/__init__.py index 39df89af..04203cff 100755 --- a/src/oidcop/authz/__init__.py +++ b/src/oidcop/authz/__init__.py @@ -30,9 +30,7 @@ def usage_rules(self, client_id: Optional[str] = ""): return _usage_rules try: - _per_client = self.server_get("endpoint_context").cdb[client_id][ - "token_usage_rules" - ] + _per_client = self.server_get("endpoint_context").cdb[client_id]["token_usage_rules"] except KeyError: pass else: @@ -59,14 +57,11 @@ 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) + session_info = self.server_get("endpoint_context").session_manager.get_session_info( + session_id=session_id, grant=True + ) grant = session_info["grant"] args = self.grant_config.copy() @@ -87,24 +82,19 @@ def __call__( # After this is where user consent should be handled scopes = request.get("scope", []) grant.scope = scopes - grant.claims = self.server_get( - "endpoint_context" - ).claims_interface.get_claims_all_usage(session_id=session_id, scopes=scopes) + grant.claims = self.server_get("endpoint_context").claims_interface.get_claims_all_usage( + session_id=session_id, scopes=scopes + ) return grant 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 - ) + grant = self.server_get("endpoint_context").session_manager.get_grant(session_id=session_id) for arg, val in args: setattr(grant, arg, val) return grant diff --git a/src/oidcop/client_authn.py b/src/oidcop/client_authn.py index 57086be9..f02868d9 100755 --- a/src/oidcop/client_authn.py +++ b/src/oidcop/client_authn.py @@ -131,9 +131,7 @@ def is_usable(self, request=None, authorization_token=None): def verify(self, request, **kwargs): if ( - self.server_get("endpoint_context").cdb[request["client_id"]][ - "client_secret" - ] + self.server_get("endpoint_context").cdb[request["client_id"]]["client_secret"] == request["client_secret"] ): return {"client_id": request["client_id"]} @@ -148,9 +146,7 @@ class BearerHeader(ClientSecretBasic): tag = "bearer_header" def is_usable(self, request=None, authorization_token=None): - if authorization_token is not None and authorization_token.startswith( - "Bearer " - ): + if authorization_token is not None and authorization_token.startswith("Bearer "): return True return False @@ -203,9 +199,7 @@ def verify(self, request, key_type, **kwargs): if _sign_alg and _sign_alg.startswith("HS"): if key_type == "private_key": raise AttributeError("Wrong key type") - keys = _context.keyjar.get( - "sig", "oct", ca_jwt["iss"], ca_jwt.jws_header.get("kid") - ) + keys = _context.keyjar.get("sig", "oct", ca_jwt["iss"], ca_jwt.jws_header.get("kid")) _secret = _context.cdb[ca_jwt["iss"]].get("client_secret") if _secret and keys[0].key != as_bytes(_secret): raise AttributeError("Oct key used for signing not client_secret") @@ -353,12 +347,7 @@ def verify_client( authorization_token = None auth_info = {} - _methods = [] - if endpoint: - try: - _methods = endpoint.client_authn_method - except AttributeError: - pass + _methods = getattr(endpoint, 'client_authn_method', []) for _method in _methods: if _method is None: @@ -366,14 +355,10 @@ 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) - ) + logger.warning("Verifying auth using {} failed: {}".format(_method.tag, err)) else: if "method" not in auth_info: auth_info["method"] = _method.tag @@ -403,9 +388,7 @@ def verify_client( raise UnknownClient("Unknown Client ID") if not valid_client_info(_cinfo): - logger.warning( - "Client registration has timed out or " "client secret is expired." - ) + logger.warning("Client registration has timed out or " "client secret is expired.") raise InvalidClient("Not valid client") # store what authn method was used @@ -413,9 +396,7 @@ def verify_client( _request_type = request.__class__.__name__ _used_authn_method = endpoint_context.cdb[client_id].get("auth_method") if _used_authn_method: - endpoint_context.cdb[client_id]["auth_method"][ - _request_type - ] = auth_info["method"] + endpoint_context.cdb[client_id]["auth_method"][_request_type] = auth_info["method"] else: endpoint_context.cdb[client_id]["auth_method"] = { _request_type: auth_info["method"] @@ -427,9 +408,7 @@ def verify_client( try: # get_client_id_from_token is a callback... Do not abuse for code readability. - auth_info["client_id"] = get_client_id_from_token( - endpoint_context, _token, request - ) + auth_info["client_id"] = get_client_id_from_token(endpoint_context, _token, request) except KeyError: raise ValueError("Unknown token") diff --git a/src/oidcop/configure.py b/src/oidcop/configure.py index 0e9f67ef..567cab44 100755 --- a/src/oidcop/configure.py +++ b/src/oidcop/configure.py @@ -23,7 +23,16 @@ "jwks_file" ] -DEFAULT_CONFIG = { +OP_DEFAULT_CONFIG = { + "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": { @@ -35,30 +44,22 @@ ], "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": {}}, "authz": { "class": "oidcop.authz.AuthzHandling", "kwargs": { "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"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, }, "expires_in": 43200, } @@ -66,30 +67,21 @@ }, "httpc_params": {"verify": False}, "issuer": "https://{domain}:{port}", - "session_key": { - "filename": "private/session_jwk.json", - "type": "OCT", - "use": "sig", - }, + "session_key": {"filename": "private/session_jwk.json", "type": "OCT", "use": "sig", }, "template_dir": "templates", "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}, - }, - "id_token": { - "class": "oidcop.token.id_token.IDToken", - "kwargs": {} - }, + "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": {}}, }, } +AS_DEFAULT_CONFIG = copy.deepcopy(OP_DEFAULT_CONFIG) +AS_DEFAULT_CONFIG["claims_interface"] = { + "class": "oidcop.session.claims.OAuth2ClaimsInterface", "kwargs": {}} + def add_base_path(conf: Union[dict, str], base_path: str, file_attributes: List[str]): if isinstance(conf, str): @@ -169,10 +161,7 @@ class Base: 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, ): if file_attributes is None: file_attributes = DEFAULT_FILE_ATTRIBUTE_NAMES @@ -200,8 +189,8 @@ def items(self): yield key, getattr(self, key) -class OPConfiguration(Base): - "Provider configuration" +class EntityConfiguration(Base): + default_config = AS_DEFAULT_CONFIG def __init__( self, @@ -221,19 +210,18 @@ def __init__( self.authentication = None self.base_url = "" self.capabilities = None + self.claims_interface = None self.cookie_handler = None self.endpoint = {} self.httpc_params = {} - self.id_token = None self.issuer = "" self.keys = None - self.login_hint2acrs = {} - self.login_hint_lookup = None self.session_key = None - self.sub_func = {} self.template_dir = None self.token_handler_args = {} self.userinfo = None + self.password = None + self.salt = None if file_attributes is None: file_attributes = DEFAULT_FILE_ATTRIBUTE_NAMES @@ -241,8 +229,8 @@ def __init__( for key in self.__dict__.keys(): _val = conf.get(key) if not _val: - if key in DEFAULT_CONFIG: - _dc = copy.deepcopy(DEFAULT_CONFIG[key]) + if key in self.default_config: + _dc = copy.deepcopy(self.default_config[key]) add_base_path(_dc, base_path, file_attributes) _val = _dc else: @@ -263,6 +251,47 @@ def __init__( set_domain_and_port(conf, URIS, domain=domain, port=port) +class OPConfiguration(EntityConfiguration): + "Provider configuration" + default_config = OP_DEFAULT_CONFIG + + 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, + ): + # OP special + self.id_token = None + self.login_hint2acrs = {} + self.login_hint_lookup = None + self.sub_func = {} + + EntityConfiguration.__init__(self, conf=conf, base_path=base_path, + entity_conf=entity_conf, domain=domain, port=port, + file_attributes=file_attributes) + + +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, + ): + 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""" @@ -343,17 +372,11 @@ 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"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, }, "expires_in": 43200, } @@ -361,15 +384,12 @@ def __init__( }, "authentication": { "user": { - "acr": "oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD", + "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"}, - }, + "db": {"class": "oidcop.util.JSONDictDB", "kwargs": {"filename": "passwd.json"}, }, "page_header": "Testing log in", "submit_btn": "Get me in!", "user_label": "Nickname", @@ -397,11 +417,8 @@ 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": { @@ -418,10 +435,7 @@ 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", @@ -431,10 +445,7 @@ 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", @@ -472,9 +483,7 @@ def __init__( "userinfo": { "path": "userinfo", "class": "oidcop.oidc.userinfo.UserInfo", - "kwargs": { - "claim_types_supported": ["normal", "aggregated", "distributed"] - }, + "kwargs": {"claim_types_supported": ["normal", "aggregated", "distributed"]}, }, "end_session": { "path": "session", @@ -506,16 +515,10 @@ def __init__( "login_hint2acrs": { "class": "oidcop.login_hint.LoginHint2Acrs", "kwargs": { - "scheme_map": { - "email": ["oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD"] - } + "scheme_map": {"email": ["urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword"]} }, }, - "session_key": { - "filename": "private/session_jwk.json", - "type": "OCT", - "use": "sig", - }, + "session_key": {"filename": "private/session_jwk.json", "type": "OCT", "use": "sig", }, "template_dir": "templates", "token_handler_args": { "jwks_def": { @@ -546,8 +549,5 @@ def __init__( }, }, }, - "userinfo": { - "class": "oidcop.user_info.UserInfo", - "kwargs": {"db_file": "users.json"}, - }, + "userinfo": {"class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": "users.json"}, }, } diff --git a/src/oidcop/construct.py b/src/oidcop/construct.py index e0374c59..3ef728f7 100644 --- a/src/oidcop/construct.py +++ b/src/oidcop/construct.py @@ -7,7 +7,7 @@ from cryptojwt.jws.jws import SIGNER_ALGS ALG_SORT_ORDER = {"RS": 0, "ES": 1, "HS": 2, "PS": 3, "no": 4} -WEAK_ALGS = ['RSA1_5', 'none'] +WEAK_ALGS = ["RSA1_5", "none"] logger = logging.getLogger(__name__) @@ -76,7 +76,7 @@ def construct_endpoint_info(default_capabilities, **kwargs): elif "encryption_enc_values_supported" in attr: _info[attr] = assign_algorithms("encryption_enc") - if re.match(r'.*(alg|enc).*_values_supported', attr): + if re.match(r".*(alg|enc).*_values_supported", attr): for i in _info[attr]: if i in WEAK_ALGS: logger.warning( diff --git a/src/oidcop/cookie_handler.py b/src/oidcop/cookie_handler.py index 1e533fa4..4524c94f 100755 --- a/src/oidcop/cookie_handler.py +++ b/src/oidcop/cookie_handler.py @@ -143,9 +143,7 @@ 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: diff --git a/src/oidcop/endpoint.py b/src/oidcop/endpoint.py index 52d13491..3152cbb3 100755 --- a/src/oidcop/endpoint.py +++ b/src/oidcop/endpoint.py @@ -115,17 +115,13 @@ def __init__(self, server_get: Callable, **kwargs): self.client_authn_method = [] if _methods: self.client_authn_method = client_auth_setup(_methods, server_get) - elif ( - _methods is not None - ): # [] or '' or something not None but regarded as nothing. + elif _methods is not None: # [] or '' or something not None but regarded as nothing. self.client_authn_method = [None] # Ignore default value elif self.default_capabilities: _methods = self.default_capabilities.get("client_authn_method") if _methods: self.client_authn_method = client_auth_setup(_methods, server_get) - self.endpoint_info = construct_endpoint_info( - self.default_capabilities, **kwargs - ) + self.endpoint_info = construct_endpoint_info(self.default_capabilities, **kwargs) # This is for matching against aud in JWTs # By default the endpoint's endpoint URL is an allowed target @@ -137,10 +133,7 @@ def parse_cookies(self, cookies: List[dict], context: EndpointContext, name: str return res def parse_request( - self, - request: Union[Message, dict, str], - http_info: Optional[dict] = None, - **kwargs + self, request: Union[Message, dict, str], http_info: Optional[dict] = None, **kwargs ): """ @@ -201,7 +194,8 @@ def parse_request( 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, **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, @@ -211,9 +205,7 @@ def get_client_id_from_token( ): return "" - def client_authentication( - self, request: Message, http_info: Optional[dict] = None, **kwargs - ): + def client_authentication(self, request: Message, http_info: Optional[dict] = None, **kwargs): """ Do client authentication @@ -234,11 +226,7 @@ def client_authentication( ) LOGGER.debug("authn_info: %s", authn_info) - if ( - authn_info == {} - and self.client_authn_method - and len(self.client_authn_method) - ): + if authn_info == {} and self.client_authn_method and len(self.client_authn_method): LOGGER.debug("client_authn_method: %s", self.client_authn_method) raise UnAuthorizedClient("Authorization failed") @@ -255,16 +243,11 @@ def do_post_parse_request( return request def do_pre_construct( - self, - response_args: dict, - request: Optional[Union[Message, dict]] = None, - **kwargs + self, response_args: dict, request: Optional[Union[Message, dict]] = None, **kwargs ) -> dict: _context = self.server_get("endpoint_context") for meth in self.pre_construct: - response_args = meth( - response_args, request, endpoint_context=_context, **kwargs - ) + response_args = meth(response_args, request, endpoint_context=_context, **kwargs) return response_args @@ -276,9 +259,7 @@ def do_post_construct( ) -> dict: _context = self.server_get("endpoint_context") for meth in self.post_construct: - response_args = meth( - response_args, request, endpoint_context=_context, **kwargs - ) + response_args = meth(response_args, request, endpoint_context=_context, **kwargs) return response_args diff --git a/src/oidcop/endpoint_context.py b/src/oidcop/endpoint_context.py index a00668cb..dab66128 100755 --- a/src/oidcop/endpoint_context.py +++ b/src/oidcop/endpoint_context.py @@ -195,9 +195,7 @@ def __init__( if _loader is None: _template_dir = conf.get("template_dir") if _template_dir: - _loader = Environment( - loader=FileSystemLoader(_template_dir), autoescape=True - ) + _loader = Environment(loader=FileSystemLoader(_template_dir), autoescape=True) if _loader: self.template_handler = Jinja2TemplateHandler(_loader) @@ -280,9 +278,7 @@ def do_userinfo(self): self.userinfo = init_user_info(_conf, self.cwd) self.session_manager.userinfo = self.userinfo else: - logger.warning( - "Cannot init_user_info if no session manager was provided." - ) + logger.warning("Cannot init_user_info if no session manager was provided.") def do_cookie_handler(self): _conf = self.conf.get("cookie_handler") @@ -328,9 +324,7 @@ def create_providerinfo(self, capabilities): _provider_info["jwks_uri"] = self.jwks_uri if "scopes_supported" not in _provider_info: - _provider_info["scopes_supported"] = [ - s for s in self.scope2claims.keys() - ] + _provider_info["scopes_supported"] = [s for s in self.scope2claims.keys()] if "claims_supported" not in _provider_info: _provider_info["claims_supported"] = STANDARD_CLAIMS[:] diff --git a/src/oidcop/exception.py b/src/oidcop/exception.py index c9aa652d..48a25093 100755 --- a/src/oidcop/exception.py +++ b/src/oidcop/exception.py @@ -96,3 +96,7 @@ class CapabilitiesMisMatch(OidcEndpointError): class MultipleCodeUsage(OidcEndpointError): pass + + +class InvalidToken(Exception): + pass diff --git a/src/oidcop/logging.py b/src/oidcop/logging.py index bd07fbd8..d04c577e 100755 --- a/src/oidcop/logging.py +++ b/src/oidcop/logging.py @@ -10,18 +10,14 @@ LOGGING_DEFAULT = { "version": 1, - "formatters": { - "default": {"format": "%(asctime)s %(name)s %(levelname)s %(message)s"} - }, + "formatters": {"default": {"format": "%(asctime)s %(name)s %(levelname)s %(message)s"}}, "handlers": {"default": {"class": "logging.StreamHandler", "formatter": "default"}}, "root": {"handlers": ["default"], "level": "INFO"}, } 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 da4c7355..cd64a3c9 100644 --- a/src/oidcop/login_hint.py +++ b/src/oidcop/login_hint.py @@ -21,6 +21,9 @@ def __call__(self, arg): 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/add_on/dpop.py b/src/oidcop/oauth2/add_on/dpop.py index 7e69c735..472c5ea6 100644 --- a/src/oidcop/oauth2/add_on/dpop.py +++ b/src/oidcop/oauth2/add_on/dpop.py @@ -1,6 +1,7 @@ from typing import Optional from cryptojwt import JWS +from cryptojwt import as_unicode from cryptojwt.jwk.jwk import key_from_jwk_dict from cryptojwt.jws.jws import factory from oidcmsg.message import SINGLE_REQUIRED_INT @@ -58,6 +59,7 @@ def create_header(self) -> str: payload = {k: self[k] for k in self.body_params} _jws = JWS(payload, alg=self["alg"]) _headers = {k: self[k] for k in self.header_params} + self.key.kid = "" _sjwt = _jws.sign_compact(keys=[self.key], **_headers) return _sjwt @@ -112,28 +114,27 @@ def post_parse_request(request, client_id, endpoint_context, **kwargs): if not _dpop.key: _dpop.key = key_from_jwk_dict(_dpop["jwk"]) - _jkt = str(_dpop.key.thumbprint("SHA-256")) - try: - endpoint_context.cdb[client_id]["dpop_jkt"][_jkt] = _dpop.key - except KeyError: - endpoint_context.cdb[client_id]["dpop_jkt"] = {_jkt: _dpop.key} - # Need something I can add as a reference when minting tokens - request["dpop_jkt"] = _jkt + request["dpop_jkt"] = as_unicode(_dpop.key.thumbprint("SHA-256")) return request def token_args(endpoint_context, client_id, token_args: Optional[dict] = None): - if "dpop.jkt" in endpoint_context.cdb[client_id]: - token_args.update({"cnf": {"jkt": endpoint_context.cdb[client_id]["dpop_jkt"]}}) + dpop_jkt = endpoint_context.cdb[client_id]["dpop_jkt"] + _jkt = list(dpop_jkt.keys())[0] + if "dpop_jkt" in endpoint_context.cdb[client_id]: + if token_args is None: + token_args = {"cnf": {"jkt": _jkt}} + else: + token_args.update({"cnf": {"jkt": endpoint_context.cdb[client_id]["dpop_jkt"]}}) return token_args def add_support(endpoint, **kwargs): # - _endp = endpoint["token"] - _endp.post_parse_request.append(post_parse_request) + _token_endp = endpoint["token"] + _token_endp.post_parse_request.append(post_parse_request) # Endpoint Context stuff # _endp.endpoint_context.token_args_methods.append(token_args) @@ -141,11 +142,12 @@ def add_support(endpoint, **kwargs): if not _algs_supported: _algs_supported = ["RS256"] - _endp.server_get("endpoint_context").provider_info[ + _token_endp.server_get("endpoint_context").provider_info[ "dpop_signing_alg_values_supported" ] = _algs_supported - # Other endpoint this may come in handy + _endpoint_context = _token_endp.server_get("endpoint_context") + _endpoint_context.dpop_enabled = True # DPoP-bound access token in the "Authorization" header and the DPoP proof in the "DPoP" header diff --git a/src/oidcop/oauth2/add_on/dpop_token.py b/src/oidcop/oauth2/add_on/dpop_token.py deleted file mode 100644 index 359bc25a..00000000 --- a/src/oidcop/oauth2/add_on/dpop_token.py +++ /dev/null @@ -1,196 +0,0 @@ -import logging -from typing import Union - -from cryptojwt.jwe.exception import JWEException -from cryptojwt.jws.exception import NoSuitableSigningKeys -from cryptojwt.jwt import utc_time_sans_frac -from oidcmsg.message import Message - -from oidcop.oidc.token import AccessTokenHelper -from oidcop.oidc.token import RefreshTokenHelper - -logger = logging.getLogger(__name__) - - -class DPOPAccessTokenHelper(AccessTokenHelper): - def process_request(self, req: Union[Message, dict], **kwargs): - """ - - :param req: - :param kwargs: - :return: - """ - _context = self.endpoint.server_get("endpoint_context") - - _mngr = _context.session_manager - _log_debug = logger.debug - - if req["grant_type"] != "authorization_code": - return self.error_cls( - error="invalid_request", error_description="Unknown grant_type" - ) - - try: - _access_code = req["code"].replace(" ", "+") - except KeyError: # Missing code parameter - absolutely fatal - return self.error_cls( - error="invalid_request", error_description="Missing code" - ) - - _session_info = _mngr.get_session_info_by_token(_access_code, grant=True) - grant = _session_info["grant"] - - code = grant.get_token(_access_code) - _authn_req = grant.authorization_request - - # If redirect_uri was in the initial authorization request - # verify that the one given here is the correct one. - if "redirect_uri" in _authn_req: - if req["redirect_uri"] != _authn_req["redirect_uri"]: - return self.error_cls( - error="invalid_request", error_description="redirect_uri mismatch" - ) - - _log_debug("All checks OK") - - issue_refresh = False - if "issue_refresh" in kwargs: - issue_refresh = kwargs["issue_refresh"] - else: - if "offline_access" in grant.scope: - issue_refresh = True - - _response = { - "token_type": "Bearer", - "scope": grant.scope, - } - - if "dpop_jkt" in req: - token_args = {"cnf": {"jkt": req["dpop_jkt"]}} - else: - token_args = {} - - token = self._mint_token( - type="access_token", - grant=grant, - session_id=_session_info["session_id"], - client_id=_session_info["client_id"], - based_on=code, - token_args=token_args, - ) - if "dpop_jkt" in req: - if token.extension is None: - token.extension = {"dpop_jkt": req["dpop_jkt"]} - else: - token.extension["dpop_jkt"] = req["dpop_jkt"] - - _response["access_token"] = token.value - _response["expires_in"] = token.expires_at - utc_time_sans_frac() - - if issue_refresh: - refresh_token = self._mint_token( - type="refresh_token", - grant=grant, - session_id=_session_info["session_id"], - client_id=_session_info["client_id"], - based_on=code, - ) - if "dpop_jkt" in req: - if refresh_token.extension is None: - refresh_token.extension = {"dpop_jkt": req["dpop_jkt"]} - else: - refresh_token.extension["dpop_jkt"] = req["dpop_jkt"] - - _response["refresh_token"] = refresh_token.value - - code.register_usage() - - # since the grant content has changed. Make sure it's stored - _mngr[_session_info["session_id"]] = grant - - if "openid" in _authn_req["scope"]: - try: - _idtoken = _context.idtoken.make(_session_info["session_id"]) - except (JWEException, NoSuitableSigningKeys) as err: - logger.warning(str(err)) - resp = self.error_cls( - error="invalid_request", - error_description="Could not sign/encrypt id_token", - ) - return resp - - _response["id_token"] = _idtoken - - return _response - - -class DPOPRefreshTokenHelper(RefreshTokenHelper): - def process_request(self, req: Union[Message, dict], **kwargs): - _context = self.endpoint.server_get("endpoint_context") - _mngr = _context.session_manager - - if req["grant_type"] != "refresh_token": - return self.error_cls( - error="invalid_request", error_description="Wrong grant_type" - ) - - token_value = req["refresh_token"] - _session_info = _mngr.get_session_info_by_token(token_value, grant=True) - token = _mngr.find_token(_session_info["session_id"], token_value) - - _grant = _session_info["grant"] - access_token = self._mint_token( - type="access_token", - grant=_grant, - session_id=_session_info["session_id"], - client_id=_session_info["client_id"], - based_on=token, - ) - - if "dpop_jkt" in req: - if access_token.extension is None: - access_token.extension = {"dpop_jkt": req["dpop_jkt"]} - else: - access_token.extension["dpop_jkt"] = req["dpop_jkt"] - - _resp = { - "access_token": access_token.value, - "token_type": access_token.token_type, - "scope": _grant.scope, - } - - if access_token.expires_at: - _resp["expires_in"] = access_token.expires_at - utc_time_sans_frac() - - _mints = token.usage_rules.get("supports_minting") - if "refresh_token" in _mints: - refresh_token = self._mint_token( - type="refresh_token", - grant=_grant, - session_id=_session_info["session_id"], - client_id=_session_info["client_id"], - based_on=token, - ) - refresh_token.usage_rules = token.usage_rules.copy() - if "dpop_jkt" in req: - if refresh_token.extension is None: - refresh_token.extension = {"dpop_jkt": req["dpop_jkt"]} - else: - refresh_token.extension["dpop_jkt"] = req["dpop_jkt"] - - _resp["refresh_token"] = refresh_token.value - - if "id_token" in _mints: - try: - _idtoken = _context.idtoken.make(_session_info["session_id"]) - except (JWEException, NoSuitableSigningKeys) as err: - logger.warning(str(err)) - resp = self.error_cls( - error="invalid_request", - error_description="Could not sign/encrypt id_token", - ) - return resp - - _resp["id_token"] = _idtoken - - return _resp diff --git a/src/oidcop/oauth2/authorization.py b/src/oidcop/oauth2/authorization.py index 138a6128..c823588e 100755 --- a/src/oidcop/oauth2/authorization.py +++ b/src/oidcop/oauth2/authorization.py @@ -45,18 +45,9 @@ # 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 = """ @@ -151,9 +142,7 @@ def verify_uri( for val in vals: if val not in _query[key]: - raise ValueError( - "{}={} value not in query part".format(key, val) - ) + raise ValueError("{}={} value not in query part".format(key, val)) # and vice versa, every query component in the uri # must be registered @@ -166,9 +155,7 @@ def verify_uri( raise ValueError('"{}" extra in query part'.format(key)) for val in vals: if val not in rquery[key]: - raise ValueError( - "Extra {}={} value in query part".format(key, val) - ) + raise ValueError("Extra {}={} value in query part".format(key, val)) match = True break if not match: @@ -210,9 +197,7 @@ def get_uri(endpoint_context, request, uri_type): raise ParameterError(f"Missing '{uri_type}' and none registered") if len(_specs) > 1: - raise ParameterError( - f"Missing '{uri_type}' and more than one registered" - ) + raise ParameterError(f"Missing '{uri_type}' and more than one registered") uri = join_query(*_specs[0]) else: @@ -222,10 +207,7 @@ 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 @@ -245,9 +227,7 @@ def authn_args_gather( else: raise ValueError("Wrong request format") - authn_args.update( - {"authn_class_ref": authn_class_ref, "return_uri": request["redirect_uri"]} - ) + authn_args.update({"authn_class_ref": authn_class_ref, "return_uri": request["redirect_uri"]}) if "req_user" in kwargs: authn_args["as_user"] = (kwargs["req_user"],) @@ -267,16 +247,11 @@ def authn_args_gather( def check_unknown_scopes_policy(request_info, cinfo, endpoint_context): op_capabilities = endpoint_context.conf["capabilities"] - client_allowed_scopes = ( - cinfo.get("allowed_scopes") or op_capabilities["scopes_supported"] - ) + client_allowed_scopes = cinfo.get("allowed_scopes") or op_capabilities["scopes_supported"] # this prevents that authz would be released for unavailable scopes for scope in request_info["scope"]: - if ( - op_capabilities.get("deny_unknown_scopes") - and scope not in client_allowed_scopes - ): + if op_capabilities.get("deny_unknown_scopes") and scope not in client_allowed_scopes: _msg = "{} requested an unauthorized scope ({})" logger.warning(_msg.format(cinfo["client_id"], scope)) raise UnAuthorizedClientScope() @@ -365,15 +340,8 @@ def _do_request_uri(self, request, client_id, endpoint_context, **kwargs): raise ValueError("Got a request_uri I can not resolve") # Do I support request_uri ? - if ( - endpoint_context.provider_info.get( - "request_uri_parameter_supported", True - ) - is False - ): - raise ServiceError( - "Someone is using request_uri which I'm not supporting" - ) + if endpoint_context.provider_info.get("request_uri_parameter_supported", True) is False: + raise ServiceError("Someone is using request_uri which I'm not supporting") _registered = endpoint_context.cdb[client_id].get("request_uris") # Not registered should be handled else where @@ -385,9 +353,7 @@ def _do_request_uri(self, request, client_id, endpoint_context, **kwargs): raise ValueError("A request_uri outside the registered") # Fetch the request - _resp = endpoint_context.httpc.get( - _request_uri, **endpoint_context.httpc_params - ) + _resp = endpoint_context.httpc.get(_request_uri, **endpoint_context.httpc_params) if _resp.status_code == 200: args = {"keyjar": endpoint_context.keyjar, "issuer": client_id} _ver_request = self.request_cls().from_jwt(_resp.text, **args) @@ -399,16 +365,10 @@ 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(): @@ -440,12 +400,8 @@ def _post_parse_request(self, request, client_id, endpoint_context, **kwargs): _cinfo = endpoint_context.cdb.get(client_id) if not _cinfo: - logger.error( - "Client ID ({}) not in client database".format(request["client_id"]) - ) - return self.error_cls( - error="unauthorized_client", error_description="unknown client" - ) + logger.error("Client ID ({}) not in client database".format(request["client_id"])) + return self.error_cls(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): @@ -473,11 +429,16 @@ def pick_authn_method(self, request, redirect_uri, acr=None, **kwargs): if auth_id: return _context.authn_broker[auth_id] + res = None if acr: res = _context.authn_broker.pick(acr) else: - res = pick_auth(_context, request) - + try: + res = pick_auth(_context, request) + except Exception as exc: + logger.exception( + f"An error occurred while picking the authN broker: {exc}" + ) if res: return res else: @@ -491,9 +452,7 @@ 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 @@ -621,12 +580,8 @@ def setup_auth( if grant.is_active() is False: return {"function": authn, "args": authn_args} elif request != grant.authorization_request: - authn_event = _mngr.get_authentication_event( - session_id=_session_id - ) - if ( - authn_event.is_valid() is False - ): # if not valid, do new login + authn_event = _mngr.get_authentication_event(session_id=_session_id) + if authn_event.is_valid() is False: # if not valid, do new login return {"function": authn, "args": authn_args} # create new grant @@ -642,9 +597,7 @@ def setup_auth( if authn_event.is_valid() is False: # if not valid, do new login return {"function": authn, "args": authn_args} else: - _session_id = self.create_session( - request, identity["uid"], authn_class_ref, _ts, authn - ) + _session_id = self.create_session(request, identity["uid"], authn_class_ref, _ts, authn) return {"session_id": _session_id, "identity": identity, "user": user} @@ -667,11 +620,7 @@ def response_mode( _args = response_args 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: @@ -728,9 +677,7 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict if "code" in request["response_type"]: _code = self.mint_token( - token_type="authorization_code", - grant=grant, - session_id=_sinfo["session_id"], + token_type="authorization_code", grant=grant, session_id=_sinfo["session_id"], ) aresp["code"] = _code.value handled_response_type.append("code") @@ -739,16 +686,12 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict if "token" in rtype: _access_token = self.mint_token( - token_type="access_token", - grant=grant, - session_id=_sinfo["session_id"], + token_type="access_token", grant=grant, session_id=_sinfo["session_id"], ) aresp["access_token"] = _access_token.value aresp["token_type"] = "Bearer" if _access_token.expires_at: - aresp["expires_in"] = ( - _access_token.expires_at - utc_time_sans_frac() - ) + aresp["expires_in"] = _access_token.expires_at - utc_time_sans_frac() handled_response_type.append("token") else: _access_token = None @@ -784,8 +727,7 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict not_handled = rtype.difference(handled_response_type) if not_handled: resp = self.error_cls( - error="invalid_request", - error_description="unsupported_response_type", + error="invalid_request", error_description="unsupported_response_type", ) return {"response_args": resp, "fragment_enc": fragment_enc} @@ -793,9 +735,7 @@ def create_authn_response(self, request: Union[dict, Message], sid: str) -> dict return {"response_args": aresp, "fragment_enc": fragment_enc} - def post_authentication( - self, request: Union[dict, Message], session_id: str, **kwargs - ) -> dict: + def post_authentication(self, request: Union[dict, Message], session_id: str, **kwargs) -> dict: """ Things that are done after a successful authentication. @@ -813,17 +753,13 @@ def post_authentication( grant = _context.authz(session_id, request=request) if grant.is_active() is False: - return self.error_response( - response_info, "server_error", "Grant not usable" - ) + return self.error_response(response_info, "server_error", "Grant not usable") user_id, client_id, grant_id = _mngr.decrypt_session_id(session_id) try: _mngr.set([user_id, client_id, grant_id], grant) except Exception as err: - return self.error_response( - response_info, "server_error", "{}".format(err.args) - ) + return self.error_response(response_info, "server_error", "{}".format(err.args)) logger.debug("response type: %s" % request["response_type"]) @@ -835,9 +771,7 @@ def post_authentication( try: redirect_uri = get_uri(_context, request, "redirect_uri") except (RedirectURIError, ParameterError) as err: - return self.error_response( - response_info, "invalid_request", "{}".format(err.args) - ) + return self.error_response(response_info, "invalid_request", "{}".format(err.args)) else: response_info["return_uri"] = redirect_uri @@ -849,9 +783,7 @@ def post_authentication( try: response_info = self.response_mode(request, **response_info) except InvalidRequest as err: - return self.error_response( - response_info, "invalid_request", "{}".format(err.args) - ) + return self.error_response(response_info, "invalid_request", "{}".format(err.args)) _cookie_info = _context.new_cookie( name=_context.cookie_handler.name["session"], @@ -882,24 +814,17 @@ def authz_part2(self, request, session_id, **kwargs): if "check_session_iframe" in _context.provider_info: salt = rndstr() try: - authn_event = _context.session_manager.get_authentication_event( - session_id - ) + authn_event = _context.session_manager.get_authentication_event(session_id) except KeyError: return self.error_response({}, "server_error", "No such session") else: if authn_event.is_valid() is False: - return self.error_response( - {}, "server_error", "Authentication has timed out" - ) + return self.error_response({}, "server_error", "Authentication has timed out") - _state = b64e( - as_bytes(json.dumps({"authn_time": authn_event["authn_time"]})) - ) + _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"] @@ -967,9 +892,7 @@ def process_request( kwargs = self.do_request_user(request_info=request, **kwargs) - info = self.setup_auth( - request, request["redirect_uri"], cinfo, _cookies, **kwargs - ) + info = self.setup_auth(request, request["redirect_uri"], cinfo, _cookies, **kwargs) if "error" in info: return info @@ -1005,9 +928,7 @@ def __call__(self, client_id, endpoint_context, alg, alg_type): _allowed = _pinfo.get(_sup) if alg not in _allowed: - logger.error( - "Signing alg user: {} not among allowed: {}".format(alg, _allowed) - ) + logger.error("Signing alg user: {} not among allowed: {}".format(alg, _allowed)) raise ValueError("Not allowed '%s' algorithm used", alg) diff --git a/src/oidcop/oauth2/introspection.py b/src/oidcop/oauth2/introspection.py index b7474a9c..0ae077f7 100644 --- a/src/oidcop/oauth2/introspection.py +++ b/src/oidcop/oauth2/introspection.py @@ -89,9 +89,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs grant = _session_info["grant"] _token = grant.get_token(request_token) - _info = self._introspect( - _token, _session_info["client_id"], _session_info["grant"] - ) + _info = self._introspect(_token, _session_info["client_id"], _session_info["grant"]) if _info is None: return {"response_args": _resp} @@ -105,9 +103,7 @@ def process_request(self, request=None, release: Optional[list] = None, **kwargs _resp.update(_info) _resp.weed() - _claims_restriction = grant.claims.get( - "introspection" - ) + _claims_restriction = grant.claims.get("introspection") if _claims_restriction: user_info = _context.claims_interface.get_user_claims( _session_info["user_id"], _claims_restriction diff --git a/src/oidcop/oauth2/token.py b/src/oidcop/oauth2/token.py new file mode 100755 index 00000000..e7e89b3d --- /dev/null +++ b/src/oidcop/oauth2/token.py @@ -0,0 +1,420 @@ +import logging +from typing import Optional +from typing import Union + +from cryptojwt.jwe.exception import JWEException +from cryptojwt.jwt import utc_time_sans_frac + +from oidcmsg import oidc +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 oidcop import sanitize +from oidcop.endpoint import Endpoint +from oidcop.exception import ProcessError +from oidcop.session.grant import AuthorizationCode +from oidcop.session.grant import Grant +from oidcop.session.grant import RefreshToken +from oidcop.session.token import MintingNotAllowed +from oidcop.session.token import SessionToken +from oidcop.token.exception import UnknownToken +from oidcop.util import importer + +logger = logging.getLogger(__name__) + + +class TokenEndpointHelper(object): + def __init__(self, endpoint, config=None): + self.endpoint = endpoint + self.config = config + self.error_cls = self.endpoint.error_cls + + def post_parse_request( + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ): + """Context specific parsing of the request. + This is done after general request parsing and before processing + the request. + """ + raise NotImplementedError + + def process_request(self, req: Union[Message, dict], **kwargs): + """Acts on a process request.""" + raise NotImplementedError + + def _mint_token( + self, + type: str, + grant: Grant, + session_id: str, + client_id: str, + based_on: Optional[SessionToken] = None, + token_args: Optional[dict] = None, + ) -> SessionToken: + _context = self.endpoint.server_get("endpoint_context") + _mngr = _context.session_manager + usage_rules = grant.usage_rules.get(type) + if usage_rules: + _exp_in = usage_rules.get("expires_in") + else: + _exp_in = 0 + + token_args = token_args or {} + for meth in _context.token_args_methods: + token_args = meth(_context, client_id, token_args) + + if token_args: + _args = {"token_args": token_args} + else: + _args = {} + + token = grant.mint_token( + session_id, + endpoint_context=_context, + token_type=type, + token_handler=_mngr.token_handler[type], + based_on=based_on, + usage_rules=usage_rules, + **_args, + ) + + if _exp_in: + if isinstance(_exp_in, str): + _exp_in = int(_exp_in) + + if _exp_in: + token.expires_at = time_sans_frac() + _exp_in + + _context.session_manager.set(_context.session_manager.unpack_session_key(session_id), grant) + + return token + + +class AccessTokenHelper(TokenEndpointHelper): + def process_request(self, req: Union[Message, dict], **kwargs): + """ + + :param req: + :param kwargs: + :return: + """ + _context = self.endpoint.server_get("endpoint_context") + + _mngr = _context.session_manager + _log_debug = logger.debug + + if req["grant_type"] != "authorization_code": + return self.error_cls(error="invalid_request", error_description="Unknown grant_type") + + try: + _access_code = req["code"].replace(" ", "+") + except KeyError: # Missing code parameter - absolutely fatal + return self.error_cls(error="invalid_request", error_description="Missing code") + + _session_info = _mngr.get_session_info_by_token(_access_code, grant=True) + grant = _session_info["grant"] + + _based_on = grant.get_token(_access_code) + _supports_minting = _based_on.usage_rules.get("supports_minting", []) + + _authn_req = grant.authorization_request + + # If redirect_uri was in the initial authorization request + # verify that the one given here is the correct one. + if "redirect_uri" in _authn_req: + if req["redirect_uri"] != _authn_req["redirect_uri"]: + return self.error_cls( + error="invalid_request", error_description="redirect_uri mismatch" + ) + + _log_debug("All checks OK") + + issue_refresh = kwargs.get("issue_refresh", False) + + _response = { + "token_type": "Bearer", + "scope": grant.scope, + } + + if "access_token" in _supports_minting: + try: + token = self._mint_token( + type="access_token", + grant=grant, + session_id=_session_info["session_id"], + client_id=_session_info["client_id"], + based_on=_based_on, + ) + except MintingNotAllowed as err: + logger.warning(err) + else: + _response["access_token"] = token.value + if token.expires_at: + _response["expires_in"] = token.expires_at - utc_time_sans_frac() + + if issue_refresh and "refresh_token" in _supports_minting: + try: + refresh_token = self._mint_token( + type="refresh_token", + grant=grant, + session_id=_session_info["session_id"], + client_id=_session_info["client_id"], + based_on=_based_on, + ) + except MintingNotAllowed as err: + logger.warning(err) + else: + _response["refresh_token"] = refresh_token.value + + # since the grant content has changed. Make sure it's stored + _mngr[_session_info["session_id"]] = grant + + _based_on.register_usage() + + return _response + + def post_parse_request( + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ): + """ + This is where clients come to get their access tokens + + :param request: The request + :param client_id: Client identifier + :returns: + """ + + _mngr = self.endpoint.server_get("endpoint_context").session_manager + try: + _session_info = _mngr.get_session_info_by_token(request["code"], grant=True) + except (KeyError, UnknownToken): + logger.error("Access Code invalid") + return self.error_cls(error="invalid_grant", error_description="Unknown code") + + grant = _session_info["grant"] + code = grant.get_token(request["code"]) + if not isinstance(code, AuthorizationCode): + return self.error_cls(error="invalid_request", error_description="Wrong token type") + + if code.is_active() is False: + return self.error_cls(error="invalid_request", error_description="Code inactive") + + _auth_req = grant.authorization_request + + if "client_id" not in request: # Optional for access token request + request["client_id"] = _auth_req["client_id"] + + logger.debug("%s: %s" % (request.__class__.__name__, sanitize(request))) + + return request + + +class RefreshTokenHelper(TokenEndpointHelper): + def process_request(self, req: Union[Message, dict], **kwargs): + _context = self.endpoint.server_get("endpoint_context") + _mngr = _context.session_manager + + if req["grant_type"] != "refresh_token": + return self.error_cls(error="invalid_request", error_description="Wrong grant_type") + + token_value = req["refresh_token"] + _session_info = _mngr.get_session_info_by_token(token_value, grant=True) + + _grant = _session_info["grant"] + token = _grant.get_token(token_value) + access_token = self._mint_token( + type="access_token", + grant=_grant, + session_id=_session_info["session_id"], + client_id=_session_info["client_id"], + based_on=token, + ) + + _resp = { + "access_token": access_token.value, + "token_type": access_token.type, + "scope": _grant.scope, + } + + if access_token.expires_at: + _resp["expires_in"] = access_token.expires_at - utc_time_sans_frac() + + _mints = token.usage_rules.get("supports_minting") + if "refresh_token" in _mints: + refresh_token = self._mint_token( + type="refresh_token", + grant=_grant, + session_id=_session_info["session_id"], + client_id=_session_info["client_id"], + based_on=token, + ) + refresh_token.usage_rules = token.usage_rules.copy() + _resp["refresh_token"] = refresh_token.value + + token.register_usage() + + return _resp + + def post_parse_request( + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ): + """ + This is where clients come to refresh their access tokens + + :param request: The request + :param client_id: Client identifier + :returns: + """ + + request = RefreshAccessTokenRequest(**request.to_dict()) + _context = self.endpoint.server_get("endpoint_context") + try: + keyjar = _context.keyjar + except AttributeError: + keyjar = "" + + request.verify(keyjar=keyjar, opponent_id=client_id) + + _mngr = _context.session_manager + try: + _session_info = _mngr.get_session_info_by_token(request["refresh_token"], grant=True) + except KeyError: + logger.error("Access Code invalid") + return self.error_cls(error="invalid_grant") + + token = _session_info["grant"].get_token(request["refresh_token"]) + + if not isinstance(token, RefreshToken): + return self.error_cls(error="invalid_request", error_description="Wrong token type") + + if token.is_active() is False: + return self.error_cls( + error="invalid_request", error_description="Refresh token inactive" + ) + + return request + + +class Token(Endpoint): + request_cls = Message + response_cls = AccessTokenResponse + error_cls = TokenErrorResponse + request_format = "json" + request_placement = "body" + response_format = "json" + response_placement = "body" + endpoint_name = "token_endpoint" + name = "token" + default_capabilities = {"token_endpoint_auth_signing_alg_values_supported": None} + helper_by_grant_type = { + "authorization_code": AccessTokenHelper, + "refresh_token": RefreshTokenHelper, + } + + def __init__(self, server_get, new_refresh_token=False, **kwargs): + Endpoint.__init__(self, server_get, **kwargs) + self.post_parse_request.append(self._post_parse_request) + if "client_authn_method" in kwargs: + self.endpoint_info["token_endpoint_auth_methods_supported"] = kwargs[ + "client_authn_method" + ] + self.allow_refresh = False + self.new_refresh_token = new_refresh_token + self.configure_grant_types(kwargs.get("grant_types_supported")) + + def configure_grant_types(self, grant_types_supported): + if grant_types_supported is None: + self.helper = {k: v(self) for k, v in self.helper_by_grant_type.items()} + return + + self.helper = {} + # TODO: do we want to allow any grant_type? + for grant_type, grant_type_options in grant_types_supported.items(): + _conf = grant_type_options.get("kwargs", {}) + if _conf is False: + continue + + try: + grant_class = grant_type_options["class"] + except (KeyError, TypeError): + raise ProcessError( + "Token Endpoint's grant types must be True, None or a dict with a" + " 'class' key." + ) + + if isinstance(grant_class, str): + try: + grant_class = importer(grant_class) + except (ValueError, AttributeError): + raise ProcessError( + f"Token Endpoint's grant type class {grant_class} can't" " be imported." + ) + + try: + self.helper[grant_type] = grant_class(self, _conf) + except Exception as e: + raise ProcessError(f"Failed to initialize class {grant_class}: {e}") + + def _post_parse_request( + self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs + ): + _helper = self.helper.get(request["grant_type"]) + if _helper: + return _helper.post_parse_request(request, client_id, **kwargs) + else: + return self.error_cls( + error="invalid_request", + error_description=f"Unsupported grant_type: {request['grant_type']}", + ) + + def process_request(self, request: Optional[Union[Message, dict]] = None, **kwargs): + """ + + :param request: + :param kwargs: + :return: Dictionary with response information + """ + if isinstance(request, self.error_cls): + return request + + if request is None: + return self.error_cls(error="invalid_request") + + try: + _helper = self.helper.get(request["grant_type"]) + if _helper: + response_args = _helper.process_request(request, **kwargs) + else: + return self.error_cls( + error="invalid_request", + error_description=f"Unsupported grant_type: {request['grant_type']}", + ) + except JWEException as err: + return self.error_cls(error="invalid_request", error_description="%s" % err) + + if isinstance(response_args, ResponseMessage): + return response_args + + _access_token = response_args["access_token"] + _context = self.server_get("endpoint_context") + _session_info = _context.session_manager.get_session_info_by_token( + _access_token, grant=True + ) + + _cookie = _context.new_cookie( + 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, + ), + ) + + _headers = [("Content-type", "application/json")] + resp = {"response_args": response_args, "http_headers": _headers} + if _cookie: + resp["cookie"] = [_cookie] + return resp diff --git a/src/oidcop/oidc/add_on/pkce.py b/src/oidcop/oidc/add_on/pkce.py index 301f315d..75b541d6 100644 --- a/src/oidcop/oidc/add_on/pkce.py +++ b/src/oidcop/oidc/add_on/pkce.py @@ -4,7 +4,9 @@ from cryptojwt.utils import b64e from oidcmsg.oauth2 import ( - AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest + AuthorizationErrorResponse, + RefreshAccessTokenRequest, + TokenExchangeRequest, ) from oidcmsg.oidc import TokenErrorResponse @@ -41,8 +43,7 @@ def post_authn_parse(request, client_id, endpoint_context, **kwargs): """ if endpoint_context.args["pkce"]["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: @@ -87,8 +88,7 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs): :return: """ if isinstance( - request, - (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest), + request, (AuthorizationErrorResponse, RefreshAccessTokenRequest, TokenExchangeRequest), ): return request @@ -97,9 +97,7 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs): request["code"], grant=True ) except KeyError: - return TokenErrorResponse( - error="invalid_grant", error_description="Unknown access grant" - ) + return TokenErrorResponse(error="invalid_grant", error_description="Unknown access grant") _authn_req = _session_info["grant"].authorization_request @@ -114,9 +112,7 @@ def post_token_parse(request, client_id, endpoint_context, **kwargs): if not verify_code_challenge( request["code_verifier"], _authn_req["code_challenge"], _method, ): - return TokenErrorResponse( - error="invalid_grant", error_description="PKCE check failed" - ) + return TokenErrorResponse(error="invalid_grant", error_description="PKCE check failed") return request diff --git a/src/oidcop/oidc/authorization.py b/src/oidcop/oidc/authorization.py index fb4d73e8..670b42a7 100755 --- a/src/oidcop/oidc/authorization.py +++ b/src/oidcop/oidc/authorization.py @@ -43,18 +43,9 @@ 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 11e4d49c..ac58ab2d 100755 --- a/src/oidcop/oidc/registration.py +++ b/src/oidcop/oidc/registration.py @@ -99,9 +99,7 @@ def comb_uri(args): val = [] for base, query_dict in args[param]: if query_dict: - query_string = urlencode( - [(key, v) for key in query_dict for v in query_dict[key]] - ) + query_string = urlencode([(key, v) for key in query_dict for v in query_dict[key]]) val.append("%s?%s" % (base, query_string)) else: val.append(base) @@ -153,9 +151,7 @@ def match_client_request(self, request): if request[_pref] not in _context.provider_info[_prov]: raise CapabilitiesMisMatch(_pref) else: - if not set(request[_pref]).issubset( - set(_context.provider_info[_prov]) - ): + if not set(request[_pref]).issubset(set(_context.provider_info[_prov])): raise CapabilitiesMisMatch(_pref) def do_client_registration(self, request, client_id, ignore=None): @@ -186,9 +182,7 @@ def do_client_registration(self, request, client_id, ignore=None): ruri = self.verify_redirect_uris(request) _cinfo["redirect_uris"] = ruri except InvalidRedirectURIError as e: - return self.error_cls( - error="invalid_redirect_uri", error_description=str(e) - ) + return self.error_cls(error="invalid_redirect_uri", error_description=str(e)) if "request_uris" in request: _uris = [] @@ -209,10 +203,9 @@ 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) @@ -239,13 +232,11 @@ def do_client_registration(self, request, client_id, ignore=None): for iss in ["", _context.issuer]: _k.extend( _context.keyjar.get_signing_key( - ktyp, alg=request[item], owner=iss + ktyp, alg=request[item], issuer_id=iss ) ) if not _k: - logger.warning( - 'Lacking support for "{}"'.format(request[item]) - ) + logger.warning('Lacking support for "{}"'.format(request[item])) del _cinfo[item] t = {"jwks_uri": "", "jwks": None} @@ -287,9 +278,7 @@ 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" @@ -299,9 +288,7 @@ def verify_redirect_uris(registration_request): raise InvalidRedirectURIError(msg) elif p.scheme not in ["http", "https"]: # Custom scheme - raise InvalidRedirectURIError( - "Custom redirect_uri not allowed for web client" - ) + raise InvalidRedirectURIError("Custom redirect_uri not allowed for web client") elif p.fragment: raise InvalidRedirectURIError("redirect_uri contains fragment") @@ -339,17 +326,13 @@ def _verify_sector_identifier(self, request): try: si_redirects = json.loads(res.text) except ValueError: - raise InvalidSectorIdentifier( - "Error deserializing sector_identifier_uri content" - ) + raise InvalidSectorIdentifier("Error deserializing sector_identifier_uri content") if "redirect_uris" in request: logger.debug("redirect_uris: %s", request["redirect_uris"]) for uri in request["redirect_uris"]: if uri not in si_redirects: - raise InvalidSectorIdentifier( - "redirect_uri missing from sector_identifiers" - ) + raise InvalidSectorIdentifier("redirect_uri missing from sector_identifiers") return si_redirects, si_url @@ -397,8 +380,7 @@ def client_registration_setup(self, request, new_id=True, set_secret=True): 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") @@ -433,16 +415,12 @@ 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 - args = dict( - [(k, v) for k, v in _cinfo.items() if k in self.response_cls.c_param] - ) + args = dict([(k, v) for k, v in _cinfo.items() if k in self.response_cls.c_param]) comb_uri(args) response = self.response_cls(**args) @@ -478,8 +456,7 @@ 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} diff --git a/src/oidcop/oidc/session.py b/src/oidcop/oidc/session.py index ef2604a0..b3db0e42 100644 --- a/src/oidcop/oidc/session.py +++ b/src/oidcop/oidc/session.py @@ -89,28 +89,20 @@ class Session(Endpoint): def __init__(self, server_get, **kwargs): _csi = kwargs.get("check_session_iframe") if _csi and not _csi.startswith("http"): - kwargs["check_session_iframe"] = add_path( - server_get("endpoint_context").issuer, _csi - ) + kwargs["check_session_iframe"] = add_path(server_get("endpoint_context").issuer, _csi) Endpoint.__init__(self, server_get, **kwargs) self.iv = as_bytes(rndstr(24)) def _encrypt_sid(self, sid): - encrypter = AES_GCMEncrypter( - key=as_bytes(self.server_get("endpoint_context").symkey) - ) + encrypter = AES_GCMEncrypter(key=as_bytes(self.server_get("endpoint_context").symkey)) enc_msg = encrypter.encrypt(as_bytes(sid), iv=self.iv) return as_unicode(b64e(enc_msg)) def _decrypt_sid(self, enc_msg): _msg = b64d(as_bytes(enc_msg)) - encrypter = AES_GCMEncrypter( - key=as_bytes(self.server_get("endpoint_context").symkey) - ) + encrypter = AES_GCMEncrypter(key=as_bytes(self.server_get("endpoint_context").symkey)) ctx, tag = split_ctx_and_tag(_msg) - return as_unicode( - encrypter.decrypt(as_bytes(ctx), iv=self.iv, tag=as_bytes(tag)) - ) + return as_unicode(encrypter.decrypt(as_bytes(ctx), iv=self.iv, tag=as_bytes(tag))) def do_back_channel_logout(self, cinfo, sid): """ @@ -198,9 +190,7 @@ def unpack_signed_jwt(self, sjwt, sig_alg=""): else: alg = self.kwargs["signing_alg"] - sign_keys = self.server_get("endpoint_context").keyjar.get_signing_key( - alg2keytype(alg) - ) + sign_keys = self.server_get("endpoint_context").keyjar.get_signing_key(alg2keytype(alg)) _info = _jwt.verify_compact(keys=sign_keys, sigalg=alg) return _info else: @@ -209,9 +199,7 @@ def unpack_signed_jwt(self, sjwt, sig_alg=""): def logout_from_client(self, sid): _context = self.server_get("endpoint_context") _cdb = _context.cdb - _session_information = _context.session_manager.get_session_info( - sid, grant=True - ) + _session_information = _context.session_manager.get_session_info(sid, grant=True) _client_id = _session_information["client_id"] res = {} @@ -221,9 +209,7 @@ def logout_from_client(self, sid): res["blu"] = {_client_id: _spec} elif "frontchannel_logout_uri" in _cdb[_client_id]: # Construct an IFrame - _spec = do_front_channel_logout_iframe( - _cdb[_client_id], _context.issuer, sid - ) + _spec = do_front_channel_logout_iframe(_cdb[_client_id], _context.issuer, sid) if _spec: res["flu"] = {_client_id: _spec} @@ -249,9 +235,7 @@ def process_request( if "post_logout_redirect_uri" in request: if "id_token_hint" not in request: - raise InvalidRequest( - "If post_logout_redirect_uri then id_token_hint is a MUST" - ) + raise InvalidRequest("If post_logout_redirect_uri then id_token_hint is a MUST") _cookies = http_info.get("cookie") _session_info = None @@ -269,9 +253,7 @@ def process_request( _cookie_info = json.loads(_cookie_infos[0]["value"]) logger.debug("Cookie info: {}".format(_cookie_info)) try: - _session_info = _mngr.get_session_info( - _cookie_info["sid"], grant=True - ) + _session_info = _mngr.get_session_info(_cookie_info["sid"], grant=True) except KeyError: raise ValueError("Can't find any corresponding session") @@ -301,21 +283,14 @@ def process_request( _uri = request["post_logout_redirect_uri"] except KeyError: if _context.issuer.endswith("/"): - _uri = "{}{}".format( - _context.issuer, self.kwargs["post_logout_uri_path"] - ) + _uri = "{}{}".format(_context.issuer, self.kwargs["post_logout_uri_path"]) else: - _uri = "{}/{}".format( - _context.issuer, self.kwargs["post_logout_uri_path"] - ) + _uri = "{}/{}".format(_context.issuer, self.kwargs["post_logout_uri_path"]) plur = False else: plur = True verify_uri( - _context, - request, - "post_logout_redirect_uri", - client_id=_session_info["client_id"], + _context, request, "post_logout_redirect_uri", client_id=_session_info["client_id"], ) payload = { @@ -339,9 +314,7 @@ def process_request( ) sjwt = _jws.pack(payload=payload, recv=_context.issuer) - location = "{}?{}".format( - self.kwargs["logout_verify_url"], urlencode({"sjwt": sjwt}) - ) + location = "{}?{}".format(self.kwargs["logout_verify_url"], urlencode({"sjwt": sjwt})) return {"redirect_location": location} def parse_request(self, request, http_info=None, **kwargs): @@ -383,9 +356,7 @@ def parse_request(self, request, http_info=None, **kwargs): else: if ( _ith.jws_header["alg"] - not in _context.provider_info[ - "id_token_signing_alg_values_supported" - ] + not in _context.provider_info["id_token_signing_alg_values_supported"] ): raise JWSException("Unsupported signing algorithm") @@ -424,7 +395,5 @@ def kill_cookies(self): session_mngmnt = _handler.make_cookie_content( value="", name=_handler.name["session_management"], max_age=-1 ) - session = _handler.make_cookie_content( - value="", name=_handler.name["session"], max_age=-1 - ) + session = _handler.make_cookie_content(value="", name=_handler.name["session"], max_age=-1) return [session_mngmnt, session] diff --git a/src/oidcop/oidc/token.py b/src/oidcop/oidc/token.py index a528d7ad..e25a8dd0 100755 --- a/src/oidcop/oidc/token.py +++ b/src/oidcop/oidc/token.py @@ -7,94 +7,20 @@ from cryptojwt.jwt import utc_time_sans_frac from oidcmsg import oidc from oidcmsg.message import Message -from oidcmsg.oauth2 import ResponseMessage from oidcmsg.oidc import RefreshAccessTokenRequest from oidcmsg.oidc import TokenErrorResponse -from oidcmsg.time_util import time_sans_frac +from oidcop import oauth2 from oidcop import sanitize -from oidcop.endpoint import Endpoint -from oidcop.exception import ProcessError +from oidcop.oauth2.token import TokenEndpointHelper from oidcop.session.grant import AuthorizationCode -from oidcop.session.grant import Grant from oidcop.session.grant import RefreshToken from oidcop.session.token import MintingNotAllowed -from oidcop.session.token import SessionToken from oidcop.token.exception import UnknownToken -from oidcop.util import importer logger = logging.getLogger(__name__) -class TokenEndpointHelper(object): - def __init__(self, endpoint, config=None): - self.endpoint = endpoint - self.config = config - self.error_cls = self.endpoint.error_cls - - def post_parse_request(self, request: Union[Message, dict], - client_id: Optional[str] = "", - **kwargs): - """Context specific parsing of the request. - This is done after general request parsing and before processing - the request. - """ - raise NotImplementedError - - def process_request(self, req: Union[Message, dict], **kwargs): - """Acts on a process request.""" - raise NotImplementedError - - def _mint_token( - self, - type: str, - grant: Grant, - session_id: str, - client_id: str, - based_on: Optional[SessionToken] = None, - token_args: Optional[dict] = None, - ) -> SessionToken: - _context = self.endpoint.server_get("endpoint_context") - _mngr = _context.session_manager - usage_rules = grant.usage_rules.get(type) - if usage_rules: - _exp_in = usage_rules.get("expires_in") - else: - _exp_in = 0 - - token_args = token_args or {} - for meth in _context.token_args_methods: - token_args = meth(_context, client_id, token_args) - - if token_args: - _args = {"token_args": token_args} - else: - _args = {} - - token = grant.mint_token( - session_id, - endpoint_context=_context, - token_type=type, - token_handler=_mngr.token_handler[type], - based_on=based_on, - usage_rules=usage_rules, - **_args, - ) - - if _exp_in: - if isinstance(_exp_in, str): - _exp_in = int(_exp_in) - - if _exp_in: - token.expires_at = time_sans_frac() + _exp_in - - _context.session_manager.set( - _context.session_manager.unpack_session_key(session_id), grant - ) - - return token - - class AccessTokenHelper(TokenEndpointHelper): def process_request(self, req: Union[Message, dict], **kwargs): """ @@ -109,20 +35,30 @@ def process_request(self, req: Union[Message, dict], **kwargs): _log_debug = logger.debug if req["grant_type"] != "authorization_code": - return self.error_cls( - error="invalid_request", error_description="Unknown grant_type" - ) + return self.error_cls(error="invalid_request", error_description="Unknown grant_type") try: _access_code = req["code"].replace(" ", "+") except KeyError: # Missing code parameter - absolutely fatal - return self.error_cls( - error="invalid_request", error_description="Missing code" - ) + return self.error_cls(error="invalid_request", error_description="Missing code") _session_info = _mngr.get_session_info_by_token(_access_code, grant=True) grant = _session_info["grant"] + token_type = "Bearer" + + # Is DPOP supported + try: + _dpop_enabled = _context.dpop_enabled + except AttributeError: + _dpop_enabled = False + + if _dpop_enabled: + _dpop_jkt = req.get("dpop_jkt") + if _dpop_jkt: + grant.extra["dpop_jkt"] = _dpop_jkt + token_type = "DPoP" + _based_on = grant.get_token(_access_code) _supports_minting = _based_on.usage_rules.get("supports_minting", []) @@ -146,7 +82,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): issue_refresh = True _response = { - "token_type": "Bearer", + "token_type": token_type, "scope": grant.scope, } @@ -223,21 +159,15 @@ def post_parse_request( _session_info = _mngr.get_session_info_by_token(request["code"], grant=True) except (KeyError, UnknownToken): logger.error("Access Code invalid") - return self.error_cls( - error="invalid_grant", error_description="Unknown code" - ) + return self.error_cls(error="invalid_grant", error_description="Unknown code") grant = _session_info["grant"] code = grant.get_token(request["code"]) if not isinstance(code, AuthorizationCode): - return self.error_cls( - error="invalid_request", error_description="Wrong token type" - ) + return self.error_cls(error="invalid_request", error_description="Wrong token type") if code.is_active() is False: - return self.error_cls( - error="invalid_request", error_description="Code inactive" - ) + return self.error_cls(error="invalid_request", error_description="Code inactive") _auth_req = grant.authorization_request @@ -255,16 +185,21 @@ def process_request(self, req: Union[Message, dict], **kwargs): _mngr = _context.session_manager if req["grant_type"] != "refresh_token": - return self.error_cls( - error="invalid_request", error_description="Wrong grant_type" - ) + return self.error_cls(error="invalid_request", error_description="Wrong grant_type") token_value = req["refresh_token"] - _session_info = _mngr.get_session_info_by_token( - token_value, grant=True - ) - + _session_info = _mngr.get_session_info_by_token(token_value, grant=True) _grant = _session_info["grant"] + + token_type = "Bearer" + + # Is DPOP supported + if "dpop_signing_alg_values_supported" in _context.provider_info: + _dpop_jkt = req.get("dpop_jkt") + if _dpop_jkt: + _grant.extra["dpop_jkt"] = _dpop_jkt + token_type = "DPoP" + token = _grant.get_token(token_value) access_token = self._mint_token( type="access_token", @@ -276,7 +211,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): _resp = { "access_token": access_token.value, - "token_type": access_token.type, + "token_type": token_type, "scope": _grant.scope, } @@ -307,8 +242,7 @@ def process_request(self, req: Union[Message, dict], **kwargs): except (JWEException, NoSuitableSigningKeys) as err: logger.warning(str(err)) resp = self.error_cls( - error="invalid_request", - error_description="Could not sign/encrypt id_token", + error="invalid_request", error_description="Could not sign/encrypt id_token", ) return resp @@ -340,9 +274,7 @@ def post_parse_request( _mngr = _context.session_manager try: - _session_info = _mngr.get_session_info_by_token( - request["refresh_token"], grant=True - ) + _session_info = _mngr.get_session_info_by_token(request["refresh_token"], grant=True) except KeyError: logger.error("Access Code invalid") return self.error_cls(error="invalid_grant") @@ -350,9 +282,7 @@ def post_parse_request( token = _session_info["grant"].get_token(request["refresh_token"]) if not isinstance(token, RefreshToken): - return self.error_cls( - error="invalid_request", error_description="Wrong token type" - ) + return self.error_cls(error="invalid_request", error_description="Wrong token type") if token.is_active() is False: return self.error_cls( @@ -362,14 +292,8 @@ def post_parse_request( return request -HELPER_BY_GRANT_TYPE = { - "authorization_code": AccessTokenHelper, - "refresh_token": RefreshTokenHelper, -} - - -class Token(Endpoint): - request_cls = oidc.Message +class Token(oauth2.token.Token): + request_cls = Message response_cls = oidc.AccessTokenResponse error_cls = TokenErrorResponse request_format = "json" @@ -379,110 +303,7 @@ class Token(Endpoint): endpoint_name = "token_endpoint" name = "token" default_capabilities = {"token_endpoint_auth_signing_alg_values_supported": None} - - def __init__(self, server_get, new_refresh_token=False, **kwargs): - Endpoint.__init__(self, server_get, **kwargs) - self.post_parse_request.append(self._post_parse_request) - if "client_authn_method" in kwargs: - self.endpoint_info["token_endpoint_auth_methods_supported"] = kwargs[ - "client_authn_method" - ] - self.allow_refresh = False - self.new_refresh_token = new_refresh_token - self.configure_grant_types(kwargs.get("grant_types_supported")) - - def configure_grant_types(self, grant_types_supported): - if grant_types_supported is None: - self.helper = {k: v(self) for k, v in HELPER_BY_GRANT_TYPE.items()} - return - - self.helper = {} - # TODO: do we want to allow any grant_type? - for grant_type, grant_type_options in grant_types_supported.items(): - _conf = grant_type_options.get("kwargs", {}) - if _conf is False: - continue - - try: - grant_class = grant_type_options["class"] - except (KeyError, TypeError): - raise ProcessError( - "Token Endpoint's grant types must be True, None or a dict with a" - " 'class' key." - ) - - if isinstance(grant_class, str): - try: - grant_class = importer(grant_class) - except (ValueError, AttributeError): - raise ProcessError( - f"Token Endpoint's grant type class {grant_class} can't" - " be imported." - ) - - try: - self.helper[grant_type] = grant_class(self, _conf) - except Exception as e: - raise ProcessError(f"Failed to initialize class {grant_class}: {e}") - - def _post_parse_request( - self, request: Union[Message, dict], client_id: Optional[str] = "", **kwargs - ): - _helper = self.helper.get(request["grant_type"]) - if _helper: - return _helper.post_parse_request(request, client_id, **kwargs) - else: - return self.error_cls( - error="invalid_request", - error_description=f"Unsupported grant_type: {request['grant_type']}", - ) - - def process_request(self, request: Optional[Union[Message, dict]] = None, **kwargs): - """ - - :param request: - :param kwargs: - :return: Dictionary with response information - """ - if isinstance(request, self.error_cls): - return request - - if request is None: - return self.error_cls(error="invalid_request") - - try: - _helper = self.helper.get(request["grant_type"]) - if _helper: - response_args = _helper.process_request(request, **kwargs) - else: - return self.error_cls( - error="invalid_request", - error_description=f"Unsupported grant_type: {request['grant_type']}", - ) - except JWEException as err: - return self.error_cls(error="invalid_request", error_description="%s" % err) - - if isinstance(response_args, ResponseMessage): - return response_args - - _access_token = response_args["access_token"] - _context = self.server_get("endpoint_context") - _session_info = _context.session_manager.get_session_info_by_token( - _access_token, grant=True - ) - - _cookie = _context.new_cookie( - 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, - ), - ) - - _headers = [("Content-type", "application/json")] - resp = {"response_args": response_args, "http_headers": _headers} - if _cookie: - resp["cookie"] = [_cookie] - return resp + helper_by_grant_type = { + "authorization_code": AccessTokenHelper, + "refresh_token": RefreshTokenHelper, + } diff --git a/src/oidcop/oidc/userinfo.py b/src/oidcop/oidc/userinfo.py index 455860e8..3d935c47 100755 --- a/src/oidcop/oidc/userinfo.py +++ b/src/oidcop/oidc/userinfo.py @@ -34,9 +34,7 @@ class UserInfo(Endpoint): "client_authn_method": ["bearer_header"], } - def __init__( - self, server_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs - ): + def __init__(self, server_get: Callable, add_claims_by_scope: Optional[bool] = True, **kwargs): Endpoint.__init__( self, server_get, add_claims_by_scope=add_claims_by_scope, **kwargs, ) @@ -108,22 +106,16 @@ def do_response( def process_request(self, request=None, **kwargs): _mngr = self.server_get("endpoint_context").session_manager - _session_info = _mngr.get_session_info_by_token( - request["access_token"], grant=True - ) + _session_info = _mngr.get_session_info_by_token(request["access_token"], grant=True) _grant = _session_info["grant"] token = _grant.get_token(request["access_token"]) # should be an access token if token.type != "access_token": - return self.error_cls( - error="invalid_token", error_description="Wrong type of token" - ) + return self.error_cls(error="invalid_token", error_description="Wrong type of token") # And it should be valid if token.is_active() is False: - return self.error_cls( - error="invalid_token", error_description="Invalid Token" - ) + return self.error_cls(error="invalid_token", error_description="Invalid Token") allowed = True _auth_event = _grant.authentication_event diff --git a/src/oidcop/scopes.py b/src/oidcop/scopes.py index b12e4db4..ec772040 100644 --- a/src/oidcop/scopes.py +++ b/src/oidcop/scopes.py @@ -44,11 +44,7 @@ def convert_scopes2claims(scopes, allowed_claims=None, scope2claim_map=None): else: for scope in scopes: try: - claims = { - name: None - for name in scope2claim_map[scope] - if name in allowed_claims - } + claims = {name: None for name in scope2claim_map[scope] if name in allowed_claims} res.update(claims) except KeyError: continue diff --git a/src/oidcop/server.py b/src/oidcop/server.py index eae929bd..99349fbe 100644 --- a/src/oidcop/server.py +++ b/src/oidcop/server.py @@ -7,12 +7,12 @@ from oidcop import authz from oidcop.client_authn import client_auth_setup +from oidcop.configure import ASConfiguration from oidcop.configure import OPConfiguration from oidcop.endpoint import Endpoint from oidcop.endpoint_context import EndpointContext from oidcop.endpoint_context import init_service from oidcop.endpoint_context import init_user_info -from oidcop.session.claims import ClaimsInterface from oidcop.session.manager import create_session_manager from oidcop.user_authn.authn_context import populate_authn_broker from oidcop.util import allow_refresh_token @@ -20,9 +20,7 @@ def do_endpoints(conf, server_get): - endpoints = build_endpoints( - conf["endpoint"], server_get=server_get, issuer=conf["issuer"] - ) + endpoints = build_endpoints(conf["endpoint"], server_get=server_get, issuer=conf["issuer"]) _cap = conf.get("capabilities", {}) @@ -60,7 +58,7 @@ class Server(ImpExp): def __init__( self, - conf: Union[dict, OPConfiguration], + conf: Union[dict, OPConfiguration, ASConfiguration], keyjar: Optional[KeyJar] = None, cwd: Optional[str] = "", cookie_handler: Optional[Any] = None, @@ -69,11 +67,7 @@ def __init__( ImpExp.__init__(self) self.conf = conf self.endpoint_context = EndpointContext( - conf=conf, - keyjar=keyjar, - cwd=cwd, - cookie_handler=cookie_handler, - httpc=httpc, + conf=conf, keyjar=keyjar, cwd=cwd, cookie_handler=cookie_handler, httpc=httpc, ) self.endpoint_context.authz = self.do_authz() @@ -82,15 +76,14 @@ def __init__( self.endpoint = do_endpoints(conf, self.server_get) _cap = get_capabilities(conf, self.endpoint) - self.endpoint_context.provider_info = self.endpoint_context.create_providerinfo( - _cap - ) + self.endpoint_context.provider_info = self.endpoint_context.create_providerinfo(_cap) self.endpoint_context.do_add_on(endpoints=self.endpoint) self.endpoint_context.session_manager = create_session_manager( self.server_get, self.endpoint_context.th_args, sub_func=self.endpoint_context._sub_func, + conf=self.conf ) self.endpoint_context.do_userinfo() # Must be done after userinfo @@ -102,12 +95,8 @@ def __init__( self.client_authn_method = [] if _methods: - _endpoint.client_authn_method = client_auth_setup( - _methods, self.server_get - ) - elif ( - _methods is not None - ): # [] or '' or something not None but regarded as nothing. + _endpoint.client_authn_method = client_auth_setup(_methods, self.server_get) + elif _methods is not None: # [] or '' or something not None but regarded as nothing. _endpoint.client_authn_method = [None] # Ignore default value elif _endpoint.default_capabilities: _methods = _endpoint.default_capabilities.get("client_authn_method") @@ -122,8 +111,8 @@ def __init__( if _token_endp: _token_endp.allow_refresh = allow_refresh_token(self.endpoint_context) - self.endpoint_context.claims_interface = ClaimsInterface( - server_get=self.server_get + self.endpoint_context.claims_interface = init_service( + conf["claims_interface"], self.server_get ) _id_token_handler = self.endpoint_context.session_manager.token_handler.handler.get( @@ -181,9 +170,7 @@ def do_login_hint_lookup(self): if _kwargs: _userinfo_conf = _kwargs.get("userinfo") if _userinfo_conf: - _userinfo = init_user_info( - _userinfo_conf, self.endpoint_context.cwd - ) + _userinfo = init_user_info(_userinfo_conf, self.endpoint_context.cwd) if _userinfo is None: _userinfo = self.endpoint_context.userinfo diff --git a/src/oidcop/session/claims.py b/src/oidcop/session/claims.py index ba7e8484..603d8de8 100755 --- a/src/oidcop/session/claims.py +++ b/src/oidcop/session/claims.py @@ -25,16 +25,13 @@ def available_claims(endpoint_context): class ClaimsInterface: init_args = {"add_claims_by_scope": False, "enable_claims_per_client": False} + claims_types = ["userinfo", "introspection", "id_token", "access_token"] def __init__(self, server_get): self.server_get = server_get - def authorization_request_claims( - self, session_id: str, usage: Optional[str] = "" - ) -> dict: - _grant = self.server_get("endpoint_context").session_manager.get_grant( - session_id - ) + def authorization_request_claims(self, session_id: str, usage: Optional[str] = "") -> dict: + _grant = self.server_get("endpoint_context").session_manager.get_grant(session_id) if _grant.authorization_request and "claims" in _grant.authorization_request: return _grant.authorization_request["claims"].get(usage, {}) @@ -47,42 +44,45 @@ def _get_client_claims(self, client_id, usage): client_claims = {k: None for k in client_claims} return client_claims - def get_claims(self, session_id: str, scopes: str, usage: str) -> dict: - """ - - :param session_id: Session identifier - :param scopes: Scopes - :param usage: Where to use the claims. One of - "userinfo"/"id_token"/"introspection"/"access_token" - :return: Claims specification as a dictionary. - """ - - _context = self.server_get("endpoint_context") - # which endpoint module configuration to get the base claims from + def _get_module(self, usage, endpoint_context): module = None if usage == "userinfo": module = self.server_get("endpoint", "userinfo") elif usage == "id_token": try: - module = _context.session_manager.token_handler["id_token"] + module = endpoint_context.session_manager.token_handler["id_token"] except KeyError: raise ServiceError("No support for ID Tokens") elif usage == "introspection": module = self.server_get("endpoint", "introspection") elif usage == "access_token": try: - module = _context.session_manager.token_handler["access_token"] + module = endpoint_context.session_manager.token_handler["access_token"] except KeyError: raise ServiceError("No support for Access Tokens") + return module + + def get_claims(self, session_id: str, scopes: str, usage: str) -> dict: + """ + + :param session_id: Session identifier + :param scopes: Scopes + :param usage: Where to use the claims. One of + "userinfo"/"id_token"/"introspection"/"access_token" + :return: Claims specification as a dictionary. + """ + + _context = self.server_get("endpoint_context") + # which endpoint module configuration to get the base claims from + module = self._get_module(usage, _context) + if module: base_claims = module.kwargs.get("base_claims", {}) else: return {} - user_id, client_id, grant_id = _context.session_manager.decrypt_session_id( - session_id - ) + user_id, client_id, grant_id = _context.session_manager.decrypt_session_id(session_id) # Can there be per client specification of which claims to use. if module.kwargs.get("enable_claims_per_client"): @@ -95,20 +95,14 @@ def get_claims(self, session_id: str, scopes: str, usage: str) -> dict: # Scopes can in some cases equate to set of claims, is that used here ? if module.kwargs.get("add_claims_by_scope"): if scopes: - _scopes = _context.scopes_handler.filter_scopes( - client_id, _context, scopes - ) + _scopes = _context.scopes_handler.filter_scopes(client_id, _context, scopes) - _claims = convert_scopes2claims( - _scopes, scope2claim_map=_context.scope2claims - ) + _claims = convert_scopes2claims(_scopes, scope2claim_map=_context.scope2claims) claims.update(_claims) # Bring in claims specification from the authorization request # This only goes for ID Token and user info - request_claims = self.authorization_request_claims( - session_id=session_id, usage=usage - ) + request_claims = self.authorization_request_claims(session_id=session_id, usage=usage) # This will add claims that has not be added before and # set filters on those claims that also appears in one of the sources above @@ -119,7 +113,7 @@ def get_claims(self, session_id: str, scopes: str, usage: str) -> dict: def get_claims_all_usage(self, session_id: str, scopes: str) -> dict: _claims = {} - for usage in ["userinfo", "introspection", "id_token", "access_token"]: + for usage in self.claims_types: _claims[usage] = self.get_claims(session_id, scopes, usage) return _claims @@ -132,9 +126,7 @@ def get_user_claims(self, user_id: str, claims_restriction: dict) -> dict: """ if claims_restriction: # Get all possible claims - user_info = self.server_get("endpoint_context").userinfo( - user_id, client_id=None - ) + user_info = self.server_get("endpoint_context").userinfo(user_id, client_id=None) # Filter out the claims that can be returned return { k: user_info.get(k) @@ -194,3 +186,25 @@ def by_schema(cls, **kwa): :return: A dictionary with claims (keys) that meets the filter criteria """ return dict([(key, val) for key, val in kwa.items() if key in cls.c_param]) + + +class OAuth2ClaimsInterface(ClaimsInterface): + claims_types = ["introspection", "access_token"] + + def _get_module(self, usage, endpoint_context): + module = None + if usage == "introspection": + module = self.server_get("endpoint", "introspection") + elif usage == "access_token": + try: + module = endpoint_context.session_manager.token_handler["access_token"] + except KeyError: + raise ServiceError("No support for Access Tokens") + + return module + + def get_claims_all_usage(self, session_id: str, scopes: str) -> dict: + _claims = {} + for usage in self.claims_types: + _claims[usage] = self.get_claims(session_id, scopes, usage) + return _claims diff --git a/src/oidcop/session/database.py b/src/oidcop/session/database.py index bf75991b..cd4bdd76 100644 --- a/src/oidcop/session/database.py +++ b/src/oidcop/session/database.py @@ -1,5 +1,7 @@ import base64 +import cryptography import logging + from typing import List from typing import Optional from typing import Union @@ -36,14 +38,14 @@ class InconsistentDatabase(TypeError): class Database(ImpExp): parameter = {"db": DLDict, "key": ""} - def __init__(self, key: Optional[str] = ""): + def __init__(self, key: Optional[str] = "", **kwargs): ImpExp.__init__(self) self.db = DLDict() - if not key: - key = rndstr(24) + for k,v in kwargs.items(): + setattr(self, k, v) - self.key = key + self.key = key or rndstr(24) self.crypt = Crypt(key) @staticmethod @@ -92,18 +94,18 @@ def get(self, path: List[str]) -> Union[SessionInfo, Grant]: try: user_info = self.db[uid] except KeyError: - raise KeyError('No such UserID') + raise KeyError("No such UserID") except TypeError: - raise InconsistentDatabase('Missing session db') + raise InconsistentDatabase("Missing session db") else: if user_info is None: - raise KeyError('No such UserID') + raise KeyError("No such UserID") if client_id is None: return user_info if client_id not in user_info.subordinate: - raise ValueError('No session from that client for that user') + raise ValueError("No session from that client for that user") try: skey = self.session_key(uid, client_id) @@ -115,8 +117,7 @@ def get(self, path: List[str]) -> Union[SessionInfo, Grant]: return client_session_info if grant_id not in client_session_info.subordinate: - raise ValueError( - 'No such grant for that user and client') + raise ValueError("No such grant for that user and client") else: try: skey = self.session_key(uid, client_id, grant_id) @@ -135,9 +136,7 @@ def delete(self, path: List[str]): _user_info = self.db[uid] skey_uid_client = self.session_key(uid, client_id) - skey_uid_client_grant = self.session_key( - uid, client_id, grant_id or '' - ) + skey_uid_client_grant = self.session_key(uid, client_id, grant_id or "") if client_id not in _user_info.subordinate: self.db.__delitem__(client_id) @@ -188,6 +187,9 @@ def encrypted_session_id(self, *args) -> str: def decrypt_session_id(self, key: str) -> List[str]: try: plain = self.crypt.decrypt(base64.b64decode(key)) + except cryptography.fernet.InvalidToken as err: + logger.error(f"cryptography.fernet.InvalidToken: {key}") + raise ValueError(err) except Exception as err: raise ValueError(err) # order: rnd, type, sid diff --git a/src/oidcop/session/grant.py b/src/oidcop/session/grant.py index 6591c722..1f7d207d 100644 --- a/src/oidcop/session/grant.py +++ b/src/oidcop/session/grant.py @@ -1,3 +1,4 @@ +from typing import Dict from typing import List from typing import Optional from uuid import uuid1 @@ -27,11 +28,11 @@ class GrantMessage(ImpExp): } def __init__( - self, - scope: Optional[str] = "", - authorization_details: Optional[dict] = None, - claims: Optional[list] = None, - resources: Optional[list] = None, + self, + scope: Optional[str] = "", + authorization_details: Optional[dict] = None, + claims: Optional[list] = None, + resources: Optional[list] = None, ): ImpExp.__init__(self) self.scope = scope @@ -110,6 +111,7 @@ class Grant(Item): "authorization_details": {}, "authorization_request": AuthorizationRequest, "claims": {}, + "extra": {}, "issued_token": [SessionToken], "resources": [], "scope": [], @@ -124,21 +126,22 @@ class Grant(Item): } def __init__( - self, - scope: Optional[list] = None, - claims: Optional[dict] = None, - resources: Optional[list] = None, - authorization_details: Optional[dict] = None, - authorization_request: Optional[Message] = None, - authentication_event: Optional[AuthnEvent] = None, - issued_token: Optional[list] = None, - usage_rules: Optional[dict] = None, - issued_at: int = 0, - expires_in: int = 0, - expires_at: int = 0, - revoked: bool = False, - token_map: Optional[dict] = None, - sub: Optional[str] = "", + self, + scope: Optional[list] = None, + claims: Optional[dict] = None, + resources: Optional[list] = None, + authorization_details: Optional[dict] = None, + authorization_request: Optional[Message] = None, + authentication_event: Optional[AuthnEvent] = None, + issued_token: Optional[list] = None, + usage_rules: Optional[dict] = None, + issued_at: int = 0, + expires_in: int = 0, + expires_at: int = 0, + revoked: bool = False, + token_map: Optional[dict] = None, + sub: Optional[str] = "", + extra: Optional[Dict[str, str]] = None ): Item.__init__( self, @@ -157,6 +160,7 @@ def __init__( self.issued_token = issued_token or [] self.id = uuid1().hex self.sub = sub + self.extra = {} if token_map is None: self.token_map = TOKEN_MAP @@ -172,12 +176,12 @@ def get(self) -> object: ) def payload_arguments( - self, - session_id: str, - endpoint_context, - token_type: str, - scope: Optional[dict] = None, - extra_payload: Optional[dict] = None, + self, + session_id: str, + endpoint_context, + token_type: str, + scope: Optional[dict] = None, + extra_payload: Optional[dict] = None, ) -> dict: """ @@ -186,44 +190,39 @@ def payload_arguments( if not scope: scope = self.scope - payload = { - "scope": scope, - "aud": self.resources, - "jti": uuid1().hex - } + payload = {"scope": scope, "aud": self.resources, "jti": uuid1().hex} if extra_payload: payload.update(extra_payload) + _jkt = self.extra.get("dpop_jkt") + if _jkt: + payload["cnf"] = {"jkt": _jkt} + if self.authorization_request: - client_id = self.authorization_request.get('client_id') + client_id = self.authorization_request.get("client_id") if client_id: - payload.update({ - "client_id": client_id, - 'sub': client_id - }) + payload.update({"client_id": client_id, "sub": client_id}) _claims_restriction = endpoint_context.claims_interface.get_claims( session_id, scopes=scope, usage=token_type ) user_id, _, _ = endpoint_context.session_manager.decrypt_session_id(session_id) - user_info = endpoint_context.claims_interface.get_user_claims( - user_id, _claims_restriction - ) + user_info = endpoint_context.claims_interface.get_user_claims(user_id, _claims_restriction) payload.update(user_info) return payload def mint_token( - self, - session_id: str, - endpoint_context: object, - token_type: str, - token_handler: TokenHandler = None, - based_on: Optional[SessionToken] = None, - usage_rules: Optional[dict] = None, - scope: Optional[list] = None, - **kwargs, + self, + session_id: str, + endpoint_context: object, + token_type: str, + token_handler: TokenHandler = None, + based_on: Optional[SessionToken] = None, + usage_rules: Optional[dict] = None, + scope: Optional[list] = None, + **kwargs, ) -> Optional[SessionToken]: """ @@ -254,12 +253,8 @@ def mint_token( token_class = self.token_map.get(token_type) if token_type == "id_token": - class_args = { - k: v for k, v in kwargs.items() if k not in ["code", "access_token"] - } - handler_args = { - k: v for k, v in kwargs.items() if k in ["code", "access_token"] - } + class_args = {k: v for k, v in kwargs.items() if k not in ["code", "access_token"]} + handler_args = {k: v for k, v in kwargs.items() if k in ["code", "access_token"]} else: class_args = kwargs handler_args = {} @@ -274,15 +269,17 @@ def mint_token( ) if token_handler is None: token_handler = endpoint_context.session_manager.token_handler.handler[ - GRANT_TYPE_MAP[token_type]] + GRANT_TYPE_MAP[token_type] + ] - token_payload = self.payload_arguments(session_id, - endpoint_context, - token_type=token_type, - scope=scope, - extra_payload=handler_args) - item.value = token_handler(session_id=session_id, - **token_payload) + token_payload = self.payload_arguments( + session_id, + endpoint_context, + token_type=token_type, + scope=scope, + extra_payload=handler_args, + ) + item.value = token_handler(session_id=session_id, **token_payload) else: raise ValueError("Can not mint that kind of token") @@ -298,11 +295,11 @@ def get_token(self, value: str) -> Optional[SessionToken]: return None def revoke_token( - self, - value: Optional[str] = "", - based_on: Optional[str] = "", - recursive: bool = True, - ): + self, + value: Optional[str] = "", + based_on: Optional[str] = "", + recursive: bool = True + ): for t in self.issued_token: if not value and not based_on: t.revoked = True @@ -341,9 +338,7 @@ def get_spec(self, token: SessionToken) -> Optional[dict]: "expires_in": 300, }, "access_token": {"supports_minting": [], "expires_in": 3600}, - "refresh_token": { - "supports_minting": ["access_token", "refresh_token", "id_token"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token", "id_token"]}, } @@ -376,19 +371,19 @@ class ExchangeGrant(Grant): type = "exchange_grant" def __init__( - self, - scope: Optional[list] = None, - claims: Optional[dict] = None, - resources: Optional[list] = None, - authorization_details: Optional[dict] = None, - issued_token: Optional[list] = None, - usage_rules: Optional[dict] = None, - issued_at: int = 0, - expires_in: int = 0, - expires_at: int = 0, - revoked: bool = False, - token_map: Optional[dict] = None, - users: list = None, + self, + scope: Optional[list] = None, + claims: Optional[dict] = None, + resources: Optional[list] = None, + authorization_details: Optional[dict] = None, + issued_token: Optional[list] = None, + usage_rules: Optional[dict] = None, + issued_at: int = 0, + expires_in: int = 0, + expires_at: int = 0, + revoked: bool = False, + token_map: Optional[dict] = None, + users: list = None, ): Grant.__init__( self, diff --git a/src/oidcop/session/info.py b/src/oidcop/session/info.py index 1b96acfa..1c2ebcb9 100644 --- a/src/oidcop/session/info.py +++ b/src/oidcop/session/info.py @@ -1,6 +1,5 @@ from typing import List from typing import Optional -from typing import Tuple from oidcmsg.impexp import ImpExp @@ -9,11 +8,11 @@ class SessionInfo(ImpExp): parameter = {"subordinate": [], "revoked": bool, "type": "", "extra_args": {}} def __init__( - self, - subordinate: Optional[List[str]] = None, - revoked: Optional[bool] = False, - type: Optional[str] = "", - **kwargs + self, + subordinate: Optional[List[str]] = None, + revoked: Optional[bool] = False, + type: Optional[str] = "", + **kwargs ): ImpExp.__init__(self) self.subordinate = subordinate or [] @@ -44,7 +43,7 @@ def keys(self): class UserSessionInfo(SessionInfo): parameter = SessionInfo.parameter.copy() parameter.update( - {"user_id": "", } + {"user_id": "",} ) def __init__(self, **kwargs): diff --git a/src/oidcop/session/manager.py b/src/oidcop/session/manager.py index 23c6a255..7e1f85cc 100644 --- a/src/oidcop/session/manager.py +++ b/src/oidcop/session/manager.py @@ -10,7 +10,6 @@ from oidcop import rndstr from oidcop.authn_event import AuthnEvent from oidcop.exception import ConfigurationError -from oidcop.session.info import SessionInfo from oidcop.token import handler from oidcop.util import Crypt from .database import Database @@ -37,10 +36,10 @@ def __init__(self, salt: Optional[str] = "", filename: Optional[str] = ""): elif filename: if os.path.isfile(filename): self.salt = open(filename).read() - elif not os.path.isfile(filename) and os.path.exists(filename): # Not a file, Something else - raise ConfigurationError( - "Salt filename points to something that is not a file" - ) + elif not os.path.isfile(filename) and os.path.exists( + filename + ): # Not a file, Something else + raise ConfigurationError("Salt filename points to something that is not a file") else: self.salt = rndstr(24) # May raise an exception @@ -73,20 +72,19 @@ class SessionManager(Database): init_args = ["handler"] def __init__( - self, - handler: TokenHandler, - conf: Optional[dict] = None, - sub_func: Optional[dict] = None, + self, handler: TokenHandler, conf: Optional[dict] = None, sub_func: Optional[dict] = None, ): - if conf: - _key = conf.get("password", rndstr(24)) - else: - _key = rndstr(24) + self.conf = conf or {} - Database.__init__(self, key=_key) + # these won't change runtime + self._key = self.conf.get("password") or rndstr(24) + self._salt = self.conf.get("salt") or rndstr(32) + + self.key = self.load_key() + self.salt = self.load_key() + + self._init_db() self.token_handler = handler - self.salt = rndstr(32) - self.conf = conf or {} # this allows the subject identifier minters to be defined by someone # else then me. @@ -105,11 +103,36 @@ def __init__( if "ephemeral" not in sub_func: self.sub_func["ephemeral"] = ephemeral_id + def load_key(self): + """returns the original key assigned in init""" + return self._key + + def load_salt(self): + """returns the original salt assigned in init""" + return self._salt + + def __setattr__(self, key, value): + if key in ('_key', '_salt'): + if hasattr(self, key): + # not first time we configure it! + raise AttributeError( + f"{key} is a ReadOnly attribute " + "that can't be overwritten!" + ) + super().__setattr__(key, value) + + def _init_db(self): + Database.__init__( + self, + key=self.load_key(), + salt=self.load_salt() + ) + def get_user_info(self, uid: str) -> UserSessionInfo: usi = self.get([uid]) if isinstance(usi, UserSessionInfo): return usi - else: # pragma: no cover + else: # pragma: no cover raise ValueError("Not UserSessionInfo") def find_token(self, session_id: str, token_value: str) -> Optional[SessionToken]: @@ -125,17 +148,17 @@ def find_token(self, session_id: str, token_value: str) -> Optional[SessionToken if token.value == token_value: return token - return None # pragma: no cover + return None # pragma: no cover def create_grant( - self, - authn_event: AuthnEvent, - auth_req: AuthorizationRequest, - user_id: str, - client_id: Optional[str] = "", - sub_type: Optional[str] = "public", - token_usage_rules: Optional[dict] = None, - scopes: Optional[list] = None, + self, + authn_event: AuthnEvent, + auth_req: AuthorizationRequest, + user_id: str, + client_id: Optional[str] = "", + sub_type: Optional[str] = "public", + token_usage_rules: Optional[dict] = None, + scopes: Optional[list] = None, ) -> str: """ @@ -165,14 +188,14 @@ def create_grant( return self.encrypted_session_id(user_id, client_id, grant.id) def create_session( - self, - authn_event: AuthnEvent, - auth_req: AuthorizationRequest, - user_id: str, - client_id: Optional[str] = "", - sub_type: Optional[str] = "public", - token_usage_rules: Optional[dict] = None, - scopes: Optional[list] = None, + self, + authn_event: AuthnEvent, + auth_req: AuthorizationRequest, + user_id: str, + client_id: Optional[str] = "", + sub_type: Optional[str] = "public", + token_usage_rules: Optional[dict] = None, + scopes: Optional[list] = None, ) -> str: """ Create part of a user session. The parts added are user- and client @@ -228,7 +251,7 @@ def get_client_session_info(self, session_id: str) -> ClientSessionInfo: csi = self.get([_user_id, _client_id]) if isinstance(csi, ClientSessionInfo): return csi - else: # pragma: no cover + else: # pragma: no cover raise ValueError("Wrong type of session info") def get_user_session_info(self, session_id: str) -> UserSessionInfo: @@ -242,7 +265,7 @@ def get_user_session_info(self, session_id: str) -> UserSessionInfo: usi = self.get([_user_id]) if isinstance(usi, UserSessionInfo): return usi - else: # pragma: no cover + else: # pragma: no cover raise ValueError("Wrong type of session info") def get_grant(self, session_id: str) -> Grant: @@ -256,13 +279,13 @@ def get_grant(self, session_id: str) -> Grant: grant = self.get([_user_id, _client_id, _grant_id]) if isinstance(grant, Grant): return grant - else: # pragma: no cover + else: # pragma: no cover raise ValueError("Wrong type of item") def _revoke_dependent(self, grant: Grant, token: SessionToken): for t in grant.issued_token: if t.based_on == token.value: - t.revoked = True # TODO: not covered yet! + t.revoked = True # TODO: not covered yet! self._revoke_dependent(grant, t) def revoke_token(self, session_id: str, token_value: str, recursive: bool = False): @@ -275,19 +298,19 @@ def revoke_token(self, session_id: str, token_value: str, recursive: bool = Fals tokens minted by this token. Recursively. """ token = self.find_token(session_id, token_value) - if token is None: # pragma: no cover + if token is None: # pragma: no cover raise UnknownToken() token.revoked = True - if recursive: # TODO: not covered yet! + if recursive: # TODO: not covered yet! grant = self[session_id] self._revoke_dependent(grant, token) def get_authentication_events( - self, - session_id: Optional[str] = "", - user_id: Optional[str] = "", - client_id: Optional[str] = "", + self, + session_id: Optional[str] = "", + user_id: Optional[str] = "", + client_id: Optional[str] = "", ) -> List[AuthnEvent]: """ Return the authentication events that exists for a user/client combination. @@ -346,10 +369,10 @@ def revoke_grant(self, session_id: str): self.set(_path, _info) def grants( - self, - session_id: Optional[str] = "", - user_id: Optional[str] = "", - client_id: Optional[str] = "", + self, + session_id: Optional[str] = "", + user_id: Optional[str] = "", + client_id: Optional[str] = "", ) -> List[Grant]: """ Find all grant connected to a user session @@ -370,13 +393,13 @@ def grants( return [self.get([user_id, client_id, gid]) for gid in _csi.subordinate] def get_session_info( - self, - session_id: str, - user_session_info: bool = False, - client_session_info: bool = False, - grant: bool = False, - authentication_event: bool = False, - authorization_request: bool = False, + self, + session_id: str, + user_session_info: bool = False, + client_session_info: bool = False, + grant: bool = False, + authentication_event: bool = False, + authorization_request: bool = False, ) -> dict: """ Returns information connected to a session. @@ -424,23 +447,25 @@ def get_session_info( return res def get_session_info_by_token( - self, - token_value: str, - user_session_info: bool = False, - client_session_info: bool = False, - grant: bool = False, - authentication_event: bool = False, - authorization_request: bool = False, + self, + token_value: str, + user_session_info: bool = False, + client_session_info: bool = False, + grant: bool = False, + authentication_event: bool = False, + authorization_request: bool = False, ) -> dict: _token_info = self.token_handler.info(token_value) - return self.get_session_info( - _token_info["sid"], + sid = _token_info["sid"] + session_info = self.get_session_info( + sid, user_session_info=user_session_info, client_session_info=client_session_info, grant=grant, authentication_event=authentication_event, authorization_request=authorization_request, ) + return session_info def get_session_id_by_token(self, token_value: str) -> str: _token_info = self.token_handler.info(token_value) @@ -470,7 +495,11 @@ def remove_session(self, session_id: str): def local_load_adjustments(self, **kwargs): self.crypt = Crypt(self.key) + def flush(self): + super().flush() + self._init_db() + -def create_session_manager(server_get, token_handler_args, sub_func=None): +def create_session_manager(server_get, token_handler_args, sub_func=None, conf=None): _token_handler = handler.factory(server_get, **token_handler_args) - return SessionManager(_token_handler, sub_func=sub_func) + return SessionManager(_token_handler, sub_func=sub_func, conf=conf) diff --git a/src/oidcop/token/__init__.py b/src/oidcop/token/__init__.py index 50d8dc01..ba3bb1ea 100755 --- a/src/oidcop/token/__init__.py +++ b/src/oidcop/token/__init__.py @@ -31,9 +31,7 @@ def __init__(self, typ, lifetime=300, **kwargs): self.lifetime = lifetime self.kwargs = kwargs - def __call__( - self, session_id: Optional[str] = "", ttype: Optional[str] = "", **payload - ) -> str: + def __call__(self, session_id: Optional[str] = "", ttype: Optional[str] = "", **payload) -> str: """ Return a token. @@ -72,9 +70,7 @@ def __init__(self, password, typ="", token_type="Bearer", **kwargs): self.crypt = Crypt(password) self.token_type = token_type - def __call__( - self, session_id: Optional[str] = "", ttype: Optional[str] = "", **payload - ) -> str: + def __call__(self, session_id: Optional[str] = "", ttype: Optional[str] = "", **payload) -> str: """ Return a token. diff --git a/src/oidcop/token/handler.py b/src/oidcop/token/handler.py index 782ae8dd..85800c8f 100755 --- a/src/oidcop/token/handler.py +++ b/src/oidcop/token/handler.py @@ -3,13 +3,13 @@ from typing import Optional import warnings -from cryptography.fernet import InvalidToken from cryptojwt.exception import Invalid from cryptojwt.key_jar import init_key_jar from cryptojwt.utils import as_unicode from oidcmsg.impexp import ImpExp from oidcmsg.item import DLDict +from oidcop.exception import InvalidToken from oidcop.token import DefaultToken from oidcop.token import Token from oidcop.token import UnknownToken @@ -25,11 +25,11 @@ class TokenHandler(ImpExp): parameter = {"handler": DLDict, "handler_order": [""]} def __init__( - self, - access_token_handler: Optional[Token] = None, - code_handler: Optional[Token] = None, - refresh_token_handler: Optional[Token] = None, - id_token_handler: Optional[Token] = None, + self, + access_token_handler: Optional[Token] = None, + code_handler: Optional[Token] = None, + refresh_token_handler: Optional[Token] = None, + id_token_handler: Optional[Token] = None, ): ImpExp.__init__(self) self.handler = {"code": code_handler, "access_token": access_token_handler} @@ -72,7 +72,7 @@ def get_handler(self, token, order=None): for typ in order: try: res = self.handler[typ].info(token) - except (KeyError, WrongTokenType, InvalidToken, UnknownToken, Invalid): + except (KeyError, WrongTokenType, InvalidToken, UnknownToken, Invalid, AttributeError): pass else: return self.handler[typ], res @@ -142,13 +142,13 @@ def default_token(spec): def factory( - server_get, - code: Optional[dict] = None, - token: Optional[dict] = None, - refresh: Optional[dict] = None, - id_token: Optional[dict] = None, - jwks_file: Optional[str] = "", - **kwargs + server_get, + code: Optional[dict] = None, + token: Optional[dict] = None, + refresh: Optional[dict] = None, + id_token: Optional[dict] = None, + jwks_file: Optional[str] = "", + **kwargs ) -> TokenHandler: """ Create a token handler @@ -165,12 +165,12 @@ def factory( key_defs = [] read_only = False cwd = server_get("endpoint_context").cwd - if kwargs.get('jwks_def'): - defs = kwargs['jwks_def'] + if kwargs.get("jwks_def"): + defs = kwargs["jwks_def"] if not jwks_file: - jwks_file = defs.get('private_path', os.path.join(cwd, JWKS_FILE)) - read_only = defs.get('read_only', read_only) - key_defs = defs.get('key_defs', []) + jwks_file = defs.get("private_path", os.path.join(cwd, JWKS_FILE)) + read_only = defs.get("read_only", read_only) + key_defs = defs.get("key_defs", []) if not jwks_file: jwks_file = os.path.join(cwd, JWKS_FILE) @@ -179,22 +179,20 @@ def factory( for kid, cnf in [("code", code), ("refresh", refresh), ("token", token)]: if cnf is not None: if default_token(cnf): - key_defs.append( - {"type": "oct", "bytes": 24, "use": ["enc"], "kid": kid} - ) + key_defs.append({"type": "oct", "bytes": 24, "use": ["enc"], "kid": kid}) kj = init_key_jar(key_defs=key_defs, private_path=jwks_file, read_only=read_only) args = {} - for typ, cnf, attr in [("code", code, "code_handler"), - ("token", token, "access_token_handler"), - ("refresh", refresh, "refresh_token_handler")]: + for typ, cnf, attr in [ + ("code", code, "code_handler"), + ("token", token, "access_token_handler"), + ("refresh", refresh, "refresh_token_handler"), + ]: if cnf is not None: if default_token(cnf): _add_passwd(kj, cnf, typ) - args[attr] = init_token_handler( - server_get, cnf, TTYPE[typ] - ) + args[attr] = init_token_handler(server_get, cnf, TTYPE[typ]) if id_token is not None: args["id_token_handler"] = init_token_handler(server_get, id_token, typ="") diff --git a/src/oidcop/token/id_token.py b/src/oidcop/token/id_token.py index 8e137331..609e29d8 100755 --- a/src/oidcop/token/id_token.py +++ b/src/oidcop/token/id_token.py @@ -13,6 +13,7 @@ from oidcop.token import is_expired from . import Token from . import UnknownToken +from ..exception import InvalidToken from ..util import get_logout_id logger = logging.getLogger(__name__) @@ -58,14 +59,12 @@ def include_session_id(endpoint_context, client_id, where): def get_sign_and_encrypt_algorithms( - endpoint_context, client_info, payload_type, sign=False, encrypt=False + endpoint_context, client_info, payload_type, sign=False, encrypt=False ): args = {"sign": sign, "encrypt": encrypt} if sign: try: - args["sign_alg"] = client_info[ - "{}_signed_response_alg".format(payload_type) - ] + args["sign_alg"] = client_info["{}_signed_response_alg".format(payload_type)] except KeyError: # Fall back to default try: args["sign_alg"] = endpoint_context.jwx_def["signing_alg"][payload_type] @@ -88,9 +87,7 @@ def get_sign_and_encrypt_algorithms( args["enc_alg"] = client_info["%s_encrypted_response_alg" % payload_type] except KeyError: try: - args["enc_alg"] = endpoint_context.jwx_def["encryption_alg"][ - payload_type - ] + args["enc_alg"] = endpoint_context.jwx_def["encryption_alg"][payload_type] except KeyError: _supported = endpoint_context.provider_info.get( "{}_encryption_alg_values_supported".format(payload_type) @@ -102,9 +99,7 @@ def get_sign_and_encrypt_algorithms( args["enc_enc"] = client_info["%s_encrypted_response_enc" % payload_type] except KeyError: try: - args["enc_enc"] = endpoint_context.jwx_def["encryption_enc"][ - payload_type - ] + args["enc_enc"] = endpoint_context.jwx_def["encryption_enc"][payload_type] except KeyError: _supported = endpoint_context.provider_info.get( "{}_encryption_enc_values_supported".format(payload_type) @@ -123,23 +118,21 @@ class IDToken(Token): } def __init__( - self, - typ: Optional[str] = "I", - lifetime: Optional[int] = 300, - server_get: Callable = None, - **kwargs + self, + typ: Optional[str] = "I", + lifetime: Optional[int] = 300, + server_get: Callable = None, + **kwargs ): Token.__init__(self, typ, **kwargs) self.lifetime = lifetime self.server_get = server_get self.kwargs = kwargs self.scope_to_claims = None - self.provider_info = construct_endpoint_info( - self.default_capabilities, **kwargs - ) + self.provider_info = construct_endpoint_info(self.default_capabilities, **kwargs) def payload( - self, session_id, alg="RS256", code=None, access_token=None, extra_claims=None, + self, session_id, alg="RS256", code=None, access_token=None, extra_claims=None, ): """ @@ -167,8 +160,7 @@ def payload( user_info = None else: user_info = _context.claims_interface.get_user_claims( - user_id=session_information["user_id"], - claims_restriction=_claims_restriction, + user_id=session_information["user_id"], claims_restriction=_claims_restriction, ) if _claims_restriction and "acr" in _claims_restriction and "acr" in _args: if claims_match(_args["acr"], _claims_restriction["acr"]) is False: @@ -193,7 +185,7 @@ def payload( _args.update(extra_claims) # Left hashes of code and/or access_token - halg = "HS%s" % alg[-3:] + halg = f"HS{alg[-3:]}" if code: _args["c_hash"] = left_hash(code.encode("utf-8"), halg) if access_token: @@ -209,15 +201,15 @@ def payload( return _args def sign_encrypt( - self, - session_id, - client_id, - code=None, - access_token=None, - sign=True, - encrypt=False, - lifetime=None, - extra_claims=None, + self, + session_id, + client_id, + code=None, + access_token=None, + sign=True, + encrypt=False, + lifetime=None, + extra_claims=None, ) -> str: """ Signed and or encrypt a IDToken @@ -255,18 +247,15 @@ def sign_encrypt( return _jwt.pack(_payload, recv=client_id) - def __call__( - self, session_id: Optional[str] = "", ttype: Optional[str] = "", **kwargs - ) -> str: + def __call__(self, session_id: Optional[str] = "", ttype: Optional[str] = "", **kwargs) -> str: _context = self.server_get("endpoint_context") - user_id, client_id, grant_id = _context.session_manager.decrypt_session_id( - session_id - ) + user_id, client_id, grant_id = _context.session_manager.decrypt_session_id(session_id) # Should I add session ID. This is about Single Logout. - if include_session_id(_context, client_id, "back") or \ - include_session_id(_context, client_id, "front"): + if include_session_id(_context, client_id, "back") or include_session_id( + _context, client_id, "front" + ): xargs = {"sid": get_logout_id(_context, user_id=user_id, client_id=client_id)} else: @@ -275,17 +264,10 @@ def __call__( lifetime = self.kwargs.get("lifetime") # Weed out stuff that doesn't belong here - kwargs = { - k: v for k, v in kwargs.items() if k in ["encrypt", "code", "access_token"] - } + kwargs = {k: v for k, v in kwargs.items() if k in ["encrypt", "code", "access_token"]} id_token = self.sign_encrypt( - session_id, - client_id, - sign=True, - lifetime=lifetime, - extra_claims=xargs, - **kwargs + session_id, client_id, sign=True, lifetime=lifetime, extra_claims=xargs, **kwargs ) return id_token @@ -302,17 +284,15 @@ def info(self, token): _context = self.server_get("endpoint_context") _jwt = factory(token) + if not _jwt: + raise InvalidToken("Not valid token") + _payload = _jwt.jwt.payload() client_id = _payload["aud"][0] client_info = _context.cdb[client_id] - alg_dict = get_sign_and_encrypt_algorithms( - _context, client_info, "id_token", sign=True - ) + alg_dict = get_sign_and_encrypt_algorithms(_context, client_info, "id_token", sign=True) - verifier = JWT( - key_jar=_context.keyjar, - allowed_sign_algs=alg_dict["sign_alg"] - ) + verifier = JWT(key_jar=_context.keyjar, allowed_sign_algs=alg_dict["sign_alg"]) try: _payload = verifier.unpack(token) except JWSException: @@ -322,7 +302,7 @@ def info(self, token): raise ToOld("Token has expired") # All the token metadata return { - "sid": _payload.get("sid", ''), # TODO: would sid be there? + "sid": _payload.get("sid", ""), # TODO: would sid be there? # "type": _payload["ttype"], "exp": _payload["exp"], "aud": client_id, diff --git a/src/oidcop/token/jwt_token.py b/src/oidcop/token/jwt_token.py index 4e93b742..312c460d 100644 --- a/src/oidcop/token/jwt_token.py +++ b/src/oidcop/token/jwt_token.py @@ -15,17 +15,17 @@ class JWTToken(Token): def __init__( - self, - typ, - # keyjar: KeyJar = None, - issuer: str = None, - aud: Optional[list] = None, - alg: str = "ES256", - lifetime: int = 300, - server_get: Callable = None, - token_type: str = "Bearer", - password: str = "", - **kwargs + self, + typ, + # keyjar: KeyJar = None, + issuer: str = None, + aud: Optional[list] = None, + alg: str = "ES256", + lifetime: int = 300, + server_get: Callable = None, + token_type: str = "Bearer", + password: str = "", + **kwargs ): Token.__init__(self, typ, **kwargs) self.token_type = token_type @@ -46,10 +46,7 @@ def load_custom_claims(self, payload: dict = None): # inherit me and do your things here return payload - def __call__(self, - session_id: Optional[str] = '', - ttype: Optional[str] = '', - **payload) -> str: + def __call__(self, session_id: Optional[str] = "", ttype: Optional[str] = "", **payload) -> str: """ Return a token. @@ -65,20 +62,13 @@ def __call__(self, else: ttype = "A" - payload.update( - {"sid": session_id, - "ttype": ttype - } - ) + payload.update({"sid": session_id, "ttype": ttype}) payload = self.load_custom_claims(payload) # payload.update(kwargs) _context = self.server_get("endpoint_context") signer = JWT( - key_jar=_context.keyjar, - iss=self.issuer, - lifetime=self.lifetime, - sign_alg=self.alg, + key_jar=_context.keyjar, iss=self.issuer, lifetime=self.lifetime, sign_alg=self.alg, ) return signer.pack(payload) diff --git a/src/oidcop/user_authn/authn_context.py b/src/oidcop/user_authn/authn_context.py index 6ea7a199..661635ae 100755 --- a/src/oidcop/user_authn/authn_context.py +++ b/src/oidcop/user_authn/authn_context.py @@ -108,58 +108,45 @@ def default(self): return None -def pick_auth(endpoint_context, areq, all=False): +def pick_auth(endpoint_context, areq, pick_all=False): """ Pick authentication method :param areq: AuthorizationRequest instance :return: A dictionary with the authentication method and its authn class ref """ - acrs = [] - try: - if len(endpoint_context.authn_broker) == 1: - return endpoint_context.authn_broker.default() - - if "acr_values" in areq: - if not isinstance(areq["acr_values"], list): - areq["acr_values"] = [areq["acr_values"]] - acrs = areq["acr_values"] - else: # same as any - try: - acrs = areq["claims"]["id_token"]["acr"]["values"] - except KeyError: - try: - _ith = areq[verified_claim_name("id_token_hint")] - except KeyError: - try: - _hint = areq["login_hint"] - except KeyError: - pass - else: - if endpoint_context.login_hint2acrs: - acrs = endpoint_context.login_hint2acrs(_hint) - else: - try: - acrs = [_ith["acr"]] - except KeyError: - pass - - if not acrs: - return endpoint_context.authn_broker.default() - - for acr in acrs: - res = endpoint_context.authn_broker.pick(acr) - logger.debug("Picked AuthN broker for ACR %s: %s" % (str(acr), str(res))) - if res: - if all: - return res - else: - # Return the first guess by pick. - return res[0] - - except KeyError as exc: - logger.debug("An error occurred while picking the authN broker: %s" % str(exc)) + if len(endpoint_context.authn_broker) == 1: + return endpoint_context.authn_broker.default() + + if "acr_values" in areq: + if not isinstance(areq["acr_values"], list): + areq["acr_values"] = [areq["acr_values"]] + acrs = areq["acr_values"] + + else: + try: + acrs = areq["claims"]["id_token"]["acr"]["values"] + except KeyError: + _ith = verified_claim_name("id_token_hint") + if areq.get(_ith): + _ith = areq[verified_claim_name("id_token_hint")] + if _ith.get("acr"): + acrs = [_ith["acr"]] + else: + if areq.get("login_hint") and endpoint_context.login_hint2acrs: + acrs = endpoint_context.login_hint2acrs(areq["login_hint"]) + + if not acrs: + return endpoint_context.authn_broker.default() + + for acr in acrs: + res = endpoint_context.authn_broker.pick(acr) + logger.debug( + f"Picked AuthN broker for ACR {str(acr)}: {str(res)}" + ) + if res: + return res if pick_all else res[0] return None diff --git a/src/oidcop/user_authn/user.py b/src/oidcop/user_authn/user.py index 8ff48f33..b31238a4 100755 --- a/src/oidcop/user_authn/user.py +++ b/src/oidcop/user_authn/user.py @@ -3,6 +3,7 @@ import inspect import json import logging +import os import sys import time import warnings @@ -32,9 +33,9 @@ }, "se": { "title": "Logga in", - "login_title": u"Användarnamn", - "passwd_title": u"Lösenord", - "submit_text": u"Sänd", + "login_title": "Användarnamn", + "passwd_title": "Lösenord", + "submit_text": "Sänd", "client_policy_title": "Klientens sekretesspolicy", }, } @@ -75,9 +76,7 @@ def verify(self, *args, **kwargs): raise NotImplementedError def unpack_token(self, token): - return verify_signed_jwt( - token=token, keyjar=self.server_get("endpoint_context").keyjar - ) + return verify_signed_jwt(token=token, keyjar=self.server_get("endpoint_context").keyjar) def done(self, areq): """ @@ -138,7 +137,7 @@ def __init__( template="user_pass.jinja2", server_get=None, verify_endpoint="", - **kwargs + **kwargs, ): super(UserPassJinja2, self).__init__(server_get=server_get) @@ -170,9 +169,7 @@ def __call__(self, **kwargs): OnlyForTestingWarning, ) if not self.server_get: - raise Exception( - f"{self.__class__.__name__} doesn't have a working server_get" - ) + raise Exception(f"{self.__class__.__name__} doesn't have a working server_get") _context = self.server_get("endpoint_context") # Stores information need afterwards in a signed JWT that then # appears as a hidden input in the form @@ -188,9 +185,7 @@ def __call__(self, **kwargs): _label = "{}_label".format(attr) _kwargs[_label] = LABELS[_uri] - return self.template_handler.render( - self.template, action=self.action, token=jws, **_kwargs - ) + return self.template_handler.render(self.template, action=self.action, token=jws, **_kwargs) def verify(self, *args, **kwargs): username = kwargs["username"] @@ -254,7 +249,7 @@ def authenticated_as(self, client_id, cookie=None, authorization="", **kwargs): try: aesgcm = AESGCM(self.symkey) user = aesgcm.decrypt(iv, encmsg, None) - except (AssertionError, KeyError): # pragma: no-cover + except (AssertionError, KeyError): # pragma: no-cover raise FailedAuthentication("Decryption failed") res = {"uid": user} @@ -280,7 +275,7 @@ def authenticated_as(self, client_id="", cookie=None, authorization="", **kwargs :param kwargs: extra key word arguments :return: """ - if self.fail: # pragma: no-cover + if self.fail: # pragma: no-cover raise self.fail() res = {"uid": self.user} diff --git a/src/oidcop/util.py b/src/oidcop/util.py index ac47bd8e..613de27c 100755 --- a/src/oidcop/util.py +++ b/src/oidcop/util.py @@ -142,9 +142,7 @@ def lv_unpack(txt): class Crypt(object): def __init__(self, password, mode=None): - self.key = base64.urlsafe_b64encode( - hashlib.sha256(password.encode("utf-8")).digest() - ) + self.key = base64.urlsafe_b64encode(hashlib.sha256(password.encode("utf-8")).digest()) self.core = Fernet(self.key) def encrypt(self, text): @@ -200,9 +198,7 @@ def split_uri(uri): def allow_refresh_token(endpoint_context): # Are there a refresh_token handler - refresh_token_handler = endpoint_context.session_manager.token_handler.handler[ - "refresh_token" - ] + refresh_token_handler = endpoint_context.session_manager.token_handler.handler["refresh_token"] # Is refresh_token grant type supported _token_supported = False @@ -254,4 +250,4 @@ def get_logout_id(endpoint_context, user_id, client_id): _mngr = endpoint_context.session_manager _mngr.set([logout_session_id], _item) - return logout_session_id \ No newline at end of file + return logout_session_id diff --git a/src/oidcop/utils.py b/src/oidcop/utils.py index f292b497..1e30dd9a 100644 --- a/src/oidcop/utils.py +++ b/src/oidcop/utils.py @@ -7,7 +7,7 @@ import yaml -def load_json(file_name): # pragma: no cover +def load_json(file_name): # pragma: no cover with open(file_name) as fp: js = json.load(fp) return js @@ -19,7 +19,7 @@ def load_yaml_config(file_name): return c -def yaml_to_py_stream(file_name): # pragma: no cover +def yaml_to_py_stream(file_name): # pragma: no cover d = load_yaml_config(file_name) fstream = io.StringIO() for i in d: @@ -29,14 +29,14 @@ def yaml_to_py_stream(file_name): # pragma: no cover return fstream -def lower_or_upper(config, param, default=None): # pragma: no cover +def lower_or_upper(config, param, default=None): # pragma: no cover res = config.get(param.lower(), default) if not res: res = config.get(param.upper(), default) return res -def create_context(dir_path, config, **kwargs): # pragma: no cover +def create_context(dir_path, config, **kwargs): # pragma: no cover _fname = lower_or_upper(config, "server_cert") if _fname: if _fname.startswith("/"): diff --git a/tests/op_config_defaults.py b/tests/op_config_defaults.py index d148d3ae..429b42ec 100644 --- a/tests/op_config_defaults.py +++ b/tests/op_config_defaults.py @@ -6,10 +6,7 @@ "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", @@ -40,10 +37,7 @@ "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", @@ -53,10 +47,7 @@ "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", @@ -94,9 +85,7 @@ "userinfo": { "path": "userinfo", "class": "oidcop.oidc.userinfo.UserInfo", - "kwargs": { - "claim_types_supported": ["normal", "aggregated", "distributed"] - }, + "kwargs": {"claim_types_supported": ["normal", "aggregated", "distributed"]}, }, "end_session": { "path": "session", @@ -126,9 +115,7 @@ "login_hint2acrs": { "class": "oidcop.login_hint.LoginHint2Acrs", "kwargs": { - "scheme_map": { - "email": ["oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD"] - } + "scheme_map": {"email": ["oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD"]} }, }, "token_handler_args": { @@ -145,20 +132,12 @@ "class": "oidcop.token.jwt_token.JWTToken", "kwargs": { "lifetime": 3600, - "add_claims": [ - "email", - "email_verified", - "phone_number", - "phone_number_verified", - ], + "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": {"filename": "users.json"}, - }, + "userinfo": {"class": "oidcop.user_info.UserInfo", "kwargs": {"filename": "users.json"},}, } diff --git a/tests/test_00_configure.py b/tests/test_00_configure.py index 1b130ee0..d74540af 100644 --- a/tests/test_00_configure.py +++ b/tests/test_00_configure.py @@ -19,9 +19,7 @@ def test_op_configure(): _str = open(full_path("op_config.json")).read() _conf = json.loads(_str) - configuration = OPConfiguration( - conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443) assert configuration assert "add_on" in configuration authz_conf = configuration["authz"] @@ -68,9 +66,7 @@ def test_op_configure_default(): _str = open(full_path("op_config.json")).read() _conf = json.loads(_str) - configuration = OPConfiguration( - conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443) assert configuration assert "add_on" in configuration authz = configuration["authz"] @@ -78,10 +74,7 @@ def test_op_configure_default(): id_token_conf = configuration.get("id_token", {}) assert set(id_token_conf.keys()) == {"kwargs", "class"} assert id_token_conf["kwargs"] == { - "base_claims": { - "email": {"essential": True}, - "email_verified": {"essential": True}, - } + "base_claims": {"email": {"essential": True}, "email_verified": {"essential": True},} } @@ -100,19 +93,14 @@ def test_op_configure_default_from_file(): id_token_conf = configuration.get("id_token", {}) assert set(id_token_conf.keys()) == {"kwargs", "class"} assert id_token_conf["kwargs"] == { - "base_claims": { - "email": {"essential": True}, - "email_verified": {"essential": True}, - } + "base_claims": {"email": {"essential": True}, "email_verified": {"essential": True},} } def test_server_configure(): configuration = create_from_config_file( Configuration, - entity_conf=[ - {"class": OPConfiguration, "attr": "op", "path": ["op", "server_info"]} - ], + entity_conf=[{"class": OPConfiguration, "attr": "op", "path": ["op", "server_info"]}], filename=full_path("srv_config.yaml"), base_path=BASEDIR, ) @@ -156,9 +144,7 @@ def test_loggin_conf_default(): "formatter": "default", }, }, - "formatters": { - "default": {"format": "%(asctime)s %(name)s %(levelname)s %(message)s"} - }, + "formatters": {"default": {"format": "%(asctime)s %(name)s %(levelname)s %(message)s"}}, } diff --git a/tests/test_00_server.py b/tests/test_00_server.py index cb3e855b..d6bd726c 100755 --- a/tests/test_00_server.py +++ b/tests/test_00_server.py @@ -44,16 +44,8 @@ def full_path(local_file): "class": ProviderConfiguration, "kwargs": {}, }, - "registration_endpoint": { - "path": "registration", - "class": Registration, - "kwargs": {}, - }, - "authorization_endpoint": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "registration_endpoint": {"path": "registration", "class": Registration, "kwargs": {},}, + "authorization_endpoint": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token_endpoint": {"path": "token", "class": Token, "kwargs": {}}, "userinfo_endpoint": { "path": "userinfo", @@ -69,6 +61,7 @@ def full_path(local_file): "kwargs": {"user": "diana"}, } }, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, "add_on": {"pkce": {"function": add_pkce_support, "kwargs": {"essential": True}}}, "template_dir": "template", "login_hint_lookup": {"class": oidcop.login_hint.LoginHintLookup, "kwargs": {}}, @@ -105,9 +98,7 @@ def test_capabilities_default(): _str = open(full_path("op_config.json")).read() _conf = json.loads(_str) - configuration = OPConfiguration( - conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443) server = Server(configuration) assert set(server.endpoint_context.provider_info["response_types_supported"]) == { @@ -119,9 +110,7 @@ def test_capabilities_default(): "id_token token", "code id_token token", } - assert ( - server.endpoint_context.provider_info["request_uri_parameter_supported"] is True - ) + assert server.endpoint_context.provider_info["request_uri_parameter_supported"] is True def test_capabilities_subset1(): @@ -145,10 +134,7 @@ def test_capabilities_bool(): _cnf = copy(conf) _cnf["capabilities"] = {"request_uri_parameter_supported": False} server = Server(_cnf) - assert ( - server.endpoint_context.provider_info["request_uri_parameter_supported"] - is False - ) + assert server.endpoint_context.provider_info["request_uri_parameter_supported"] is False def test_cdb(): diff --git a/tests/test_01_claims.py b/tests/test_01_claims.py index 0a2ab77e..fa5f8dae 100644 --- a/tests/test_01_claims.py +++ b/tests/test_01_claims.py @@ -61,11 +61,7 @@ def full_path(local_file): "class": "oidcop.oidc.authorization.Authorization", "kwargs": {}, }, - "token_endpoint": { - "path": "token", - "class": "oidcop.oidc.token.Token", - "kwargs": {}, - }, + "token_endpoint": {"path": "token", "class": "oidcop.oidc.token.Token", "kwargs": {},}, "userinfo_endpoint": { "path": "userinfo", "class": "oidcop.oidc.userinfo.UserInfo", @@ -102,6 +98,7 @@ def full_path(local_file): "class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": full_path("users.json")}, }, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } USER_ID = "diana" @@ -143,46 +140,34 @@ def _create_session(self, auth_req, sub_type="public", sector_identifier=""): def test_authorization_request_id_token_claims(self): session_id = self._create_session(AREQ) - claims = self.claims_interface.authorization_request_claims( - session_id, "id_token" - ) + claims = self.claims_interface.authorization_request_claims(session_id, "id_token") assert claims == {} def test_authorization_request_id_token_claims_2(self): session_id = self._create_session(AREQ_2) - claims = self.claims_interface.authorization_request_claims( - session_id, "id_token" - ) + claims = self.claims_interface.authorization_request_claims(session_id, "id_token") assert claims assert set(claims.keys()) == {"nickname"} def test_authorization_request_userinfo_claims(self): session_id = self._create_session(AREQ) - claims = self.claims_interface.authorization_request_claims( - session_id, "userinfo" - ) + claims = self.claims_interface.authorization_request_claims(session_id, "userinfo") assert claims == {} def test_authorization_request_userinfo_claims_2(self): session_id = self._create_session(AREQ_2) - claims = self.claims_interface.authorization_request_claims( - session_id, "userinfo" - ) + claims = self.claims_interface.authorization_request_claims(session_id, "userinfo") assert claims == {} def test_authorization_request_userinfo_claims_3(self): session_id = self._create_session(AREQ_3) - claims = self.claims_interface.authorization_request_claims( - session_id, "userinfo" - ) + claims = self.claims_interface.authorization_request_claims(session_id, "userinfo") assert set(claims.keys()) == {"name", "email", "email_verified"} - @pytest.mark.parametrize( - "usage", ["id_token", "userinfo", "introspection", "token"] - ) + @pytest.mark.parametrize("usage", ["id_token", "userinfo", "introspection", "token"]) def test_get_client_claims_0(self, usage): claims = self.claims_interface._get_client_claims("client_1", usage) assert claims == {} @@ -202,9 +187,7 @@ def test_get_client_claims_introspection_1(self): claims = self.claims_interface._get_client_claims("client_1", "introspection") assert set(claims.keys()) == {"email"} - @pytest.mark.parametrize( - "usage", ["id_token", "userinfo", "introspection", "token"] - ) + @pytest.mark.parametrize("usage", ["id_token", "userinfo", "introspection", "token"]) def test_get_claims(self, usage): session_id = self._create_session(AREQ) claims = self.claims_interface.get_claims(session_id, [], usage) @@ -238,9 +221,7 @@ def test_get_claims_id_token_3(self): } self.endpoint_context.cdb["client_1"]["id_token_claims"] = ["name", "email"] - claims = self.claims_interface.get_claims( - session_id, ["openid", "address"], "id_token" - ) + claims = self.claims_interface.get_claims(session_id, ["openid", "address"], "id_token") assert set(claims.keys()) == { "name", "email", @@ -259,9 +240,7 @@ def test_get_claims_userinfo_3(self): } self.endpoint_context.cdb["client_1"]["userinfo_claims"] = ["name", "email"] - claims = self.claims_interface.get_claims( - session_id, ["openid", "address"], "userinfo" - ) + claims = self.claims_interface.get_claims(session_id, ["openid", "address"], "userinfo") assert set(claims.keys()) == { "name", "email", @@ -304,9 +283,7 @@ def test_get_claims_access_token_3(self): self.endpoint_context.cdb["client_1"]["access_token_claims"] = ["name", "email"] session_id = self._create_session(AREQ) - claims = self.claims_interface.get_claims( - session_id, ["openid", "address"], "access_token" - ) + claims = self.claims_interface.get_claims(session_id, ["openid", "address"], "access_token") assert set(claims.keys()) == { "name", "email", @@ -324,9 +301,7 @@ def test_get_claims_all_usage(self): self.server.server_get("endpoint", "introspection").kwargs = {} session_id = self._create_session(AREQ) - claims = self.claims_interface.get_claims_all_usage( - session_id, ["openid", "address"] - ) + claims = self.claims_interface.get_claims_all_usage(session_id, ["openid", "address"]) assert set(claims.keys()) == { "id_token", "userinfo", @@ -347,16 +322,12 @@ def test_get_claims_all_usage_2(self): } self.endpoint_context.cdb["client_1"]["userinfo_claims"] = ["name", "email"] - self.server.server_get("endpoint", "introspection").kwargs = { - "add_claims_by_scope": True - } + self.server.server_get("endpoint", "introspection").kwargs = {"add_claims_by_scope": True} self.endpoint_context.session_manager.token_handler["access_token"].kwargs = {} session_id = self._create_session(AREQ) - claims = self.claims_interface.get_claims_all_usage( - session_id, ["openid", "address"] - ) + claims = self.claims_interface.get_claims_all_usage(session_id, ["openid", "address"]) assert set(claims.keys()) == { "id_token", @@ -380,9 +351,7 @@ def test_get_user_claims(self): } self.endpoint_context.cdb["client_1"]["userinfo_claims"] = ["name", "email"] - self.server.server_get("endpoint", "introspection").kwargs = { - "add_claims_by_scope": True - } + self.server.server_get("endpoint", "introspection").kwargs = {"add_claims_by_scope": True} self.endpoint_context.session_manager.token_handler["access_token"].kwargs = {} @@ -391,14 +360,10 @@ def test_get_user_claims(self): session_id, ["openid", "address"] ) - _claims = self.claims_interface.get_user_claims( - USER_ID, claims_restriction["userinfo"] - ) + _claims = self.claims_interface.get_user_claims(USER_ID, claims_restriction["userinfo"]) assert _claims == {"name": "Diana Krall", "email": "diana@example.org"} - _claims = self.claims_interface.get_user_claims( - USER_ID, claims_restriction["id_token"] - ) + _claims = self.claims_interface.get_user_claims(USER_ID, claims_restriction["id_token"]) assert _claims == {"email_verified": False, "email": "diana@example.org"} _claims = self.claims_interface.get_user_claims( @@ -414,7 +379,5 @@ def test_get_user_claims(self): } } - _claims = self.claims_interface.get_user_claims( - USER_ID, claims_restriction["access_token"] - ) + _claims = self.claims_interface.get_user_claims(USER_ID, claims_restriction["access_token"]) assert _claims == {} diff --git a/tests/test_01_grant.py b/tests/test_01_grant.py index edb91e01..84302480 100644 --- a/tests/test_01_grant.py +++ b/tests/test_01_grant.py @@ -30,11 +30,7 @@ "class": "oidcop.oidc.authorization.Authorization", "kwargs": {}, }, - "token_endpoint": { - "path": "token", - "class": "oidcop.oidc.token.Token", - "kwargs": {}, - }, + "token_endpoint": {"path": "token", "class": "oidcop.oidc.token.Token", "kwargs": {},}, }, "authentication": { "anon": { @@ -43,6 +39,7 @@ "kwargs": {"user": "diana"}, } }, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } USER_ID = "diana" @@ -420,9 +417,7 @@ def test_get_spec(self): spec = grant.get_spec(access_token) assert set(spec.keys()) == {"scope", "claims", "resources"} assert spec["scope"] == ["openid", "email", "eduperson"] - assert spec["claims"] == { - "userinfo": {"given_name": None, "eduperson_affiliation": None} - } + assert spec["claims"] == {"userinfo": {"given_name": None, "eduperson_affiliation": None}} assert spec["resources"] == ["https://api.example.com"] def test_get_usage_rules(self): @@ -438,9 +433,7 @@ def test_get_usage_rules(self): # Default usage rules self.endpoint_context.cdb["client_id"] = {} - rules = get_usage_rules( - "access_token", self.endpoint_context, grant, "client_id" - ) + rules = get_usage_rules("access_token", self.endpoint_context, grant, "client_id") assert rules == {"supports_minting": [], "expires_in": 3600} # client specific usage rules diff --git a/tests/test_01_session_token.py b/tests/test_01_session_token.py index 34b0510d..622e4b6d 100644 --- a/tests/test_01_session_token.py +++ b/tests/test_01_session_token.py @@ -37,14 +37,15 @@ def test_authorization_code_extras(): assert code.resources == ["https://api.example.com"] -def test_dump_load(cls=AuthorizationCode, - kwargs=dict( - value="ABCD", - scope=["openid", "foo", "bar"], - claims={"userinfo": {"given_name": None}}, - resources=["https://api.example.com"], - ) - ): +def test_dump_load( + cls=AuthorizationCode, + kwargs=dict( + value="ABCD", + scope=["openid", "foo", "bar"], + claims={"userinfo": {"given_name": None}}, + resources=["https://api.example.com"], + ), +): code = cls(**kwargs) _item = code.dump() @@ -54,17 +55,13 @@ def test_dump_load(cls=AuthorizationCode, if val: assert val == getattr(_new_code, attr) + def test_dump_load_access_token(): - test_dump_load( - cls=AccessToken, - kwargs={} - ) + test_dump_load(cls=AccessToken, kwargs={}) + def test_dump_load_idtoken(): - test_dump_load( - cls=IDToken, - kwargs={} - ) + test_dump_load(cls=IDToken, kwargs={}) def test_supports_minting(): diff --git a/tests/test_01_util.py b/tests/test_01_util.py index fa246104..ea8bab84 100644 --- a/tests/test_01_util.py +++ b/tests/test_01_util.py @@ -19,27 +19,15 @@ "verify_ssl": False, "capabilities": {}, "jwks_uri": "https://example.com/jwks.json", - "keys": { - "private_path": "own/jwks.json", - "key_defs": KEYDEFS, - "uri_path": "static/jwks.json", - }, + "keys": {"private_path": "own/jwks.json", "key_defs": KEYDEFS, "uri_path": "static/jwks.json",}, "endpoint": { "provider_config": { "path": ".well-known/openid-configuration", "class": ProviderConfiguration, "kwargs": {}, }, - "registration_endpoint": { - "path": "registration", - "class": Registration, - "kwargs": {}, - }, - "authorization_endpoint": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "registration_endpoint": {"path": "registration", "class": Registration, "kwargs": {},}, + "authorization_endpoint": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token_endpoint": {"path": "token", "class": Token, "kwargs": {}}, "userinfo_endpoint": { "path": "userinfo", diff --git a/tests/test_02_authz_handling.py b/tests/test_02_authz_handling.py index 8b7bc619..c3183556 100644 --- a/tests/test_02_authz_handling.py +++ b/tests/test_02_authz_handling.py @@ -52,17 +52,11 @@ "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"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, }, "expires_in": 43200, } @@ -74,11 +68,7 @@ "class": "oidcop.oidc.authorization.Authorization", "kwargs": {}, }, - "token_endpoint": { - "path": "token", - "class": "oidcop.oidc.token.Token", - "kwargs": {}, - }, + "token_endpoint": {"path": "token", "class": "oidcop.oidc.token.Token", "kwargs": {},}, "userinfo_endpoint": { "path": "userinfo", "class": "oidcop.oidc.userinfo.UserInfo", @@ -111,6 +101,7 @@ }, "id_token": {"class": "oidcop.token.id_token.IDToken", "kwargs": {}}, }, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } USER_ID = "diana" diff --git a/tests/test_02_client_authn.py b/tests/test_02_client_authn.py index a3942a58..fae199d7 100755 --- a/tests/test_02_client_authn.py +++ b/tests/test_02_client_authn.py @@ -65,11 +65,8 @@ }, }, "template_dir": "template", - "keys": { - "private_path": "own/jwks.json", - "key_defs": KEYDEFS, - "uri_path": "static/jwks.json", - }, + "keys": {"private_path": "own/jwks.json", "key_defs": KEYDEFS, "uri_path": "static/jwks.json",}, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } client_id = "client_id" @@ -205,17 +202,13 @@ def test_private_key_jwt_reusage_other_endpoint(self): _jwt = JWT(client_keyjar, iss=client_id, sign_alg="RS256") _jwt.with_jti = True - _assertion = _jwt.pack( - {"aud": [self.method.server_get("endpoint", "token").full_path]} - ) + _assertion = _jwt.pack({"aud": [self.method.server_get("endpoint", "token").full_path]}) request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER} # This should be OK assert self.method.is_usable(request=request) - self.method.verify( - request=request, endpoint=self.method.server_get("endpoint", "token") - ) + self.method.verify(request=request, endpoint=self.method.server_get("endpoint", "token")) # This should NOT be OK with pytest.raises(NotForMe): @@ -225,9 +218,7 @@ def test_private_key_jwt_reusage_other_endpoint(self): # This should NOT be OK because this is the second time the token appears with pytest.raises(MultipleUsage): - self.method.verify( - request, endpoint=self.method.server_get("endpoint", "token") - ) + self.method.verify(request, endpoint=self.method.server_get("endpoint", "token")) def test_private_key_jwt_auth_endpoint(self): # Own dynamic keys @@ -248,8 +239,7 @@ def test_private_key_jwt_auth_endpoint(self): assert self.method.is_usable(request=request) authn_info = self.method.verify( - request=request, - endpoint=self.method.server_get("endpoint", "authorization"), + request=request, endpoint=self.method.server_get("endpoint", "authorization"), ) assert authn_info["client_id"] == client_id @@ -265,9 +255,7 @@ def create_method(self): def test_bearerheader(self): authorization_info = "Bearer 1234567890" - assert self.method.verify(authorization_token=authorization_info) == { - "token": "1234567890" - } + assert self.method.verify(authorization_token=authorization_info) == {"token": "1234567890"} def test_bearerheader_wrong_type(self): authorization_info = "Thrower 1234567890" @@ -461,9 +449,7 @@ def test_verify_client_bearer_body(self): def test_verify_client_client_secret_post(self): request = {"client_id": client_id, "client_secret": client_secret} res = verify_client( - self.endpoint_context, - request, - endpoint=self.server.server_get("endpoint", "token"), + self.endpoint_context, request, endpoint=self.server.server_get("endpoint", "token"), ) assert set(res.keys()) == {"method", "client_id"} assert res["method"] == "client_secret_post" @@ -522,9 +508,7 @@ def test_verify_client_jws_authn_method(self): request = {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER} res = verify_client( - self.endpoint_context, - request, - endpoint=self.server.server_get("endpoint", "token"), + self.endpoint_context, request, endpoint=self.server.server_get("endpoint", "token"), ) assert res["method"] == "client_secret_jwt" assert res["client_id"] == "client_id" @@ -544,9 +528,7 @@ def test_verify_client_bearer_body(self): def test_verify_client_client_secret_post(self): request = {"client_id": client_id, "client_secret": client_secret} res = verify_client( - self.endpoint_context, - request, - endpoint=self.server.server_get("endpoint", "token"), + self.endpoint_context, request, endpoint=self.server.server_get("endpoint", "token"), ) assert set(res.keys()) == {"method", "client_id"} assert res["method"] == "client_secret_post" diff --git a/tests/test_02_sess_mngm_db.py b/tests/test_02_sess_mngm_db.py index ce492edc..d5c3f0c3 100644 --- a/tests/test_02_sess_mngm_db.py +++ b/tests/test_02_sess_mngm_db.py @@ -88,9 +88,7 @@ def test_client_info_add2(self): # The reference is there but not the value del self.db.db[self.db.session_key("diana", "client_1")] - authn_event = create_authn_event( - uid="diana", expires_in=10, authn_info="authn_class_ref" - ) + authn_event = create_authn_event(uid="diana", expires_in=10, authn_info="authn_class_ref") grant = Grant(authentication_event=authn_event, authorization_request=AUTHZ_REQ) @@ -183,9 +181,7 @@ def test_step_wise(self): # store user info self.db.set(["diana"], UserSessionInfo(user_id="diana")) # Client specific information - self.db.set( - ["diana", "client_1"], ClientSessionInfo(sub=public_id("diana", salt)) - ) + self.db.set(["diana", "client_1"], ClientSessionInfo(sub=public_id("diana", salt))) # Grant grant = Grant() access_code = SessionToken("access_code", value="1234567890") diff --git a/tests/test_04_token_handler.py b/tests/test_04_token_handler.py index a1e802a6..3e263a66 100644 --- a/tests/test_04_token_handler.py +++ b/tests/test_04_token_handler.py @@ -116,12 +116,8 @@ def setup_token_handler(self): refresh_token_expires_in = 86400 code_handler = DefaultToken(password, typ="A", lifetime=grant_expires_in) - access_token_handler = DefaultToken( - password, typ="T", lifetime=token_expires_in - ) - refresh_token_handler = DefaultToken( - password, typ="R", lifetime=refresh_token_expires_in - ) + access_token_handler = DefaultToken(password, typ="T", lifetime=token_expires_in) + refresh_token_handler = DefaultToken(password, typ="R", lifetime=refresh_token_expires_in) self.handler = TokenHandler( code_handler=code_handler, @@ -177,16 +173,12 @@ def test_token_handler_from_config(): conf = { "issuer": "https://example.com/op", "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, - "endpoint": { - "endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}}, - }, + "endpoint": {"endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}},}, "token_handler_args": { "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -199,7 +191,7 @@ def test_token_handler_from_config(): }, "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", @@ -238,42 +230,39 @@ def test_token_handler_from_config(): assert token_handler.handler["refresh_token"].alg == "ES256" assert token_handler.handler["refresh_token"].kwargs == {} assert token_handler.handler["refresh_token"].lifetime == 3600 - assert token_handler.handler["refresh_token"].def_aud == [ - "https://example.org/appl" - ] + assert token_handler.handler["refresh_token"].def_aud == ["https://example.org/appl"] assert token_handler.handler["id_token"].lifetime == 300 assert "base_claims" in token_handler.handler["id_token"].kwargs -@pytest.mark.parametrize("jwks", [ - {"jwks_file": "private/token_jwks_1.json"}, - {"jwks_def": { - "private_path": "private/token_jwks_2.json", - "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], - }}, - { - "jwks_file": "private/token_jwks_1.json", - "jwks_def": { - "private_path": "private/token_jwks_2.json", - "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], - } - }, - None -]) +@pytest.mark.parametrize( + "jwks", + [ + {"jwks_file": "private/token_jwks_1.json"}, + { + "jwks_def": { + "private_path": "private/token_jwks_2.json", + "read_only": False, + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], + } + }, + { + "jwks_file": "private/token_jwks_1.json", + "jwks_def": { + "private_path": "private/token_jwks_2.json", + "read_only": False, + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], + }, + }, + None, + ], +) def test_file(jwks): conf = { "issuer": "https://example.com/op", "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, - "endpoint": { - "endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}}, - }, + "endpoint": {"endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}},}, "token_handler_args": { "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -286,7 +275,7 @@ def test_file(jwks): }, "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", @@ -305,8 +294,11 @@ def test_file(jwks): except KeyError: pass - for _file in ["private/token_jwks_1.json", "private/token_jwks_2.json", - "private/token_jwks.json"]: + for _file in [ + "private/token_jwks_1.json", + "private/token_jwks_2.json", + "private/token_jwks.json", + ]: if os.path.exists(full_path(_file)): os.unlink(full_path(_file)) diff --git a/tests/test_05_id_token.py b/tests/test_05_id_token.py index 76ba827d..8696f76c 100644 --- a/tests/test_05_id_token.py +++ b/tests/test_05_id_token.py @@ -123,9 +123,7 @@ def full_path(local_file): "max_usage": 1, }, "access_token": {}, - "refresh_token": { - "supports_minting": ["access_token", "refresh_token"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, }, "expires_in": 43200, } @@ -136,17 +134,11 @@ def full_path(local_file): "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"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"]}, }, "expires_in": 43200, } @@ -154,6 +146,7 @@ def full_path(local_file): }, "userinfo": {"class": "oidcop.user_info.UserInfo", "kwargs": {"db": USERS},}, "client_authn": verify_client, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } USER_ID = "diana" @@ -171,9 +164,7 @@ def create_session_manager(self): "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], } - self.endpoint_context.keyjar.add_symmetric( - "client_1", "hemligtochintekort", ["sig", "enc"] - ) + self.endpoint_context.keyjar.add_symmetric("client_1", "hemligtochintekort", ["sig", "enc"]) self.session_manager = self.endpoint_context.session_manager self.user_id = USER_ID @@ -211,9 +202,7 @@ def _mint_access_token(self, grant, session_id, token_ref): ) return access_token - def _mint_id_token( - self, grant, session_id, token_ref=None, code=None, access_token=None - ): + def _mint_id_token(self, grant, session_id, token_ref=None, code=None, access_token=None): return grant.mint_token( session_id=session_id, endpoint_context=self.endpoint_context, @@ -247,9 +236,7 @@ def test_id_token_payload_with_code(self): grant = self.session_manager[session_id] code = self._mint_code(grant, session_id) - id_token = self._mint_id_token( - grant, session_id, token_ref=code, code=code.value - ) + id_token = self._mint_id_token(grant, session_id, token_ref=code, code=code.value) _jwt = factory(id_token.value) payload = _jwt.jwt.payload() @@ -296,11 +283,7 @@ def test_id_token_payload_with_code_and_access_token(self): access_token = self._mint_access_token(grant, session_id, code) id_token = self._mint_id_token( - grant, - session_id, - token_ref=code, - code=code.value, - access_token=access_token.value, + grant, session_id, token_ref=code, code=code.value, access_token=access_token.value, ) _jwt = factory(id_token.value) @@ -345,11 +328,7 @@ def test_id_token_payload_many_0(self): access_token = self._mint_access_token(grant, session_id, code) id_token = self._mint_id_token( - grant, - session_id, - token_ref=code, - code=code.value, - access_token=access_token.value, + grant, session_id, token_ref=code, code=code.value, access_token=access_token.value, ) _jwt = factory(id_token.value) @@ -397,8 +376,11 @@ def test_get_sign_algorithm(self): ) # default signing alg assert algs == { - 'sign': True, 'encrypt': True, 'sign_alg': 'RS256', - 'enc_alg': 'RSA-OAEP', 'enc_enc': 'A128CBC-HS256' + "sign": True, + "encrypt": True, + "sign_alg": "RS256", + "enc_alg": "RSA-OAEP", + "enc_enc": "A128CBC-HS256", } def test_available_claims(self): @@ -433,9 +415,7 @@ def test_client_claims(self): session_id = self._create_session(AREQ) grant = self.session_manager[session_id] - self.session_manager.token_handler["id_token"].kwargs[ - "enable_claims_per_client" - ] = True + self.session_manager.token_handler["id_token"].kwargs["enable_claims_per_client"] = True self.endpoint_context.cdb["client_1"]["id_token_claims"] = {"address": None} _claims = self.endpoint_context.claims_interface.get_claims( @@ -478,9 +458,7 @@ def test_client_claims_scopes(self): session_id = self._create_session(AREQS) grant = self.session_manager[session_id] - self.session_manager.token_handler["id_token"].kwargs[ - "add_claims_by_scope" - ] = True + self.session_manager.token_handler["id_token"].kwargs["add_claims_by_scope"] = True _claims = self.endpoint_context.claims_interface.get_claims( session_id=session_id, scopes=AREQS["scope"], usage="id_token" @@ -502,9 +480,7 @@ def test_client_claims_scopes_and_request_claims_no_match(self): session_id = self._create_session(AREQRC) grant = self.session_manager[session_id] - self.session_manager.token_handler["id_token"].kwargs[ - "add_claims_by_scope" - ] = True + self.session_manager.token_handler["id_token"].kwargs["add_claims_by_scope"] = True _claims = self.endpoint_context.claims_interface.get_claims( session_id=session_id, scopes=AREQRC["scope"], usage="id_token" @@ -531,9 +507,7 @@ def test_client_claims_scopes_and_request_claims_one_match(self): session_id = self._create_session(_req) grant = self.session_manager[session_id] - self.session_manager.token_handler["id_token"].kwargs[ - "add_claims_by_scope" - ] = True + self.session_manager.token_handler["id_token"].kwargs["add_claims_by_scope"] = True _claims = self.endpoint_context.claims_interface.get_claims( session_id=session_id, scopes=_req["scope"], usage="id_token" @@ -552,7 +526,6 @@ def test_client_claims_scopes_and_request_claims_one_match(self): # Scope -> claims assert "address" in res - def test_id_token_info(self): session_id = self._create_session(AREQ) grant = self.session_manager[session_id] @@ -565,14 +538,14 @@ def test_id_token_info(self): endpoint_context = self.endpoint_context sman = endpoint_context.session_manager - server_get = sman.token_handler.handler['id_token'].server_get + server_get = sman.token_handler.handler["id_token"].server_get _info = self.session_manager.token_handler.info(id_token.value) - assert 'sid' in _info - assert 'exp' in _info - assert 'aud' in _info + assert "sid" in _info + assert "exp" in _info + assert "aud" in _info - client_id = AREQ.get('client_id') - _id_token = sman.token_handler.handler['id_token'] + client_id = AREQ.get("client_id") + _id_token = sman.token_handler.handler["id_token"] _id_token.sign_encrypt(session_id, client_id) # TODO: we need an authentication event for this id_token for a better coverage @@ -580,6 +553,5 @@ def test_id_token_info(self): client_info = endpoint_context.cdb[client_id] get_sign_and_encrypt_algorithms( - endpoint_context, client_info, payload_type="id_token", - sign=True, encrypt=True + endpoint_context, client_info, payload_type="id_token", sign=True, encrypt=True ) diff --git a/tests/test_05_jwt_token.py b/tests/test_05_jwt_token.py index fab24e71..40f5cb7f 100644 --- a/tests/test_05_jwt_token.py +++ b/tests/test_05_jwt_token.py @@ -86,7 +86,7 @@ "authorization_code": "code", "access_token": "access_token", "refresh_token": "refresh_token", - "id_token": "id_token" + "id_token": "id_token", } @@ -135,11 +135,7 @@ def create_endpoint(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "{}/registration", - "class": Registration, - "kwargs": {}, - }, + "registration": {"path": "{}/registration", "class": Registration, "kwargs": {},}, "authorization": { "path": "{}/authorization", "class": Authorization, @@ -168,11 +164,7 @@ def create_endpoint(self): "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": {}, @@ -184,6 +176,7 @@ def create_endpoint(self): } }, }, + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } server = Server(conf, keyjar=KEYJAR) self.endpoint_context = server.endpoint_context @@ -255,9 +248,9 @@ def test_info(self): def test_enable_claims_per_client(self, enable_claims_per_client): # Set up configuration self.endpoint_context.cdb["client_1"]["access_token_claims"] = {"address": None} - self.endpoint_context.session_manager.token_handler.handler[ - "access_token" - ].kwargs["enable_claims_per_client"] = enable_claims_per_client + self.endpoint_context.session_manager.token_handler.handler["access_token"].kwargs[ + "enable_claims_per_client" + ] = enable_claims_per_client session_id = self._create_session(AUTH_REQ) # apply consent diff --git a/tests/test_06_authn_context.py b/tests/test_06_authn_context.py index fa022f70..34fffbda 100644 --- a/tests/test_06_authn_context.py +++ b/tests/test_06_authn_context.py @@ -25,11 +25,7 @@ "kwargs": {"user": "diana"}, "class": "oidcop.user_authn.user.NoAuthn", }, - "krall": { - "acr": INTERNETPROTOCOLPASSWORD, - "kwargs": {"user": "krall"}, - "class": NoAuthn, - }, + "krall": {"acr": INTERNETPROTOCOLPASSWORD, "kwargs": {"user": "krall"}, "class": NoAuthn,}, } KEYDEFS = [ @@ -144,6 +140,7 @@ def create_authn_broker(self): "authentication": METHOD, "userinfo": {"class": UserInfo, "kwargs": {"db": USERINFO_db}}, "template_dir": "template", + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } cookie_conf = { "sign_key": SYMKey(k="ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch"), @@ -182,14 +179,12 @@ def test_pick_authn_one(self): def test_pick_authn_all(self): request = {"acr_values": INTERNETPROTOCOLPASSWORD} - res = pick_auth(self.server.server_get("endpoint_context"), request, all=True) + res = pick_auth(self.server.server_get("endpoint_context"), request, pick_all=True) assert len(res) == 2 def test_authn_event(): - an = AuthnEvent( - uid="uid", valid_until=time_sans_frac() + 1, authn_info="authn_class_ref", - ) + an = AuthnEvent(uid="uid", valid_until=time_sans_frac() + 1, authn_info="authn_class_ref",) assert an.is_valid() diff --git a/tests/test_06_session_manager.py b/tests/test_06_session_manager.py index d0d62363..6acb4458 100644 --- a/tests/test_06_session_manager.py +++ b/tests/test_06_session_manager.py @@ -54,9 +54,7 @@ def create_session_manager(self): "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"lifetime": 600}, "token": { @@ -82,6 +80,7 @@ def create_session_manager(self): "token_endpoint": {"path": "{}/token", "class": Token, "kwargs": {}}, }, "template_dir": "template", + "claims_interface": {"class": "oidcop.session.claims.ClaimsInterface", "kwargs": {}}, } server = Server(conf) self.server = server @@ -263,9 +262,7 @@ def test_check_grant(self): scopes=["openid", "phoe"], ) - _session_info = self.session_manager.get_session_info( - session_id=session_id, grant=True - ) + _session_info = self.session_manager.get_session_info(session_id=session_id, grant=True) grant = _session_info["grant"] assert grant.scope == ["openid", "phoe"] @@ -275,10 +272,7 @@ def test_check_grant(self): def test_find_token(self): session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) _info = self.session_manager.get_session_info(session_id=session_id, grant=True) @@ -287,9 +281,7 @@ def test_find_token(self): code = self._mint_token("authorization_code", grant, session_id) access_token = self._mint_token("access_token", grant, session_id, code) - _session_id = self.session_manager.encrypted_session_id( - "diana", "client_1", grant.id - ) + _session_id = self.session_manager.encrypted_session_id("diana", "client_1", grant.id) _token = self.session_manager.find_token(_session_id, access_token.value) assert _token.type == "access_token" @@ -304,9 +296,7 @@ def test_get_authentication_event(self): # client_id="client_1", ) - _info = self.session_manager.get_session_info( - session_id, authentication_event=True - ) + _info = self.session_manager.get_session_info(session_id, authentication_event=True) authn_event = _info["authentication_event"] assert isinstance(authn_event, AuthnEvent) @@ -314,16 +304,11 @@ def test_get_authentication_event(self): assert authn_event["authn_info"] == "authn_class_ref" # cover the remaining one ... - _info = self.session_manager.get_session_info( - session_id, authorization_request=True - ) + _info = self.session_manager.get_session_info(session_id, authorization_request=True) def test_get_client_session_info(self): _session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) csi = self.session_manager.get_client_session_info(_session_id) @@ -332,10 +317,7 @@ def test_get_client_session_info(self): def test_get_general_session_info(self): _session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) _session_info = self.session_manager.get_session_info(_session_id) @@ -351,10 +333,7 @@ def test_get_general_session_info(self): def test_get_session_info_by_token(self): _session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) grant = self.session_manager.get_grant(_session_id) @@ -372,10 +351,7 @@ def test_get_session_info_by_token(self): def test_token_usage_default(self): _session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) grant = self.session_manager[_session_id] @@ -392,16 +368,11 @@ def test_token_usage_default(self): refresh_token = self._mint_token("refresh_token", grant, _session_id, code) - assert refresh_token.usage_rules == { - "supports_minting": ["access_token", "refresh_token"] - } + assert refresh_token.usage_rules == {"supports_minting": ["access_token", "refresh_token"]} def test_token_usage_grant(self): _session_id = self.session_manager.create_session( - authn_event=self.authn_event, - auth_req=AUTH_REQ, - user_id="diana", - client_id="client_1", + authn_event=self.authn_event, auth_req=AUTH_REQ, user_id="diana", client_id="client_1", ) grant = self.session_manager[_session_id] grant.usage_rules = { @@ -411,9 +382,7 @@ def test_token_usage_grant(self): "expires_in": 300, }, "access_token": {"expires_in": 3600}, - "refresh_token": { - "supports_minting": ["access_token", "refresh_token", "id_token"] - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token", "id_token"]}, } code = self._mint_token("authorization_code", grant, _session_id) @@ -564,23 +533,16 @@ def test_authentication_events(self): assert isinstance(res[0], AuthnEvent) - res = self.session_manager.get_authentication_events( - user_id= "diana", - client_id="client_1" - ) + res = self.session_manager.get_authentication_events(user_id="diana", client_id="client_1") assert isinstance(res[0], AuthnEvent) try: - self.session_manager.get_authentication_events( - user_id="diana", - ) + self.session_manager.get_authentication_events(user_id="diana",) except AttributeError: pass else: - raise Exception( - "get_authentication_events MUST return a list of AuthnEvent" - ) + raise Exception("get_authentication_events MUST return a list of AuthnEvent") def test_user_info(self): token_usage_rules = self.endpoint_context.authz.usage_rules("client_1") @@ -604,7 +566,6 @@ def test_revoke_client_session(self): ) self.session_manager.revoke_client_session(_session_id) - def test_revoke_grant(self): token_usage_rules = self.endpoint_context.authz.usage_rules("client_1") _session_id = self.session_manager.create_session( @@ -645,27 +606,20 @@ def test_grants(self): assert isinstance(res, list) - res = self.session_manager.grants( - user_id= "diana", - client_id="client_1" - ) + res = self.session_manager.grants(user_id="diana", client_id="client_1") assert isinstance(res, list) try: - self.session_manager.grants( - user_id="diana", - ) + self.session_manager.grants(user_id="diana",) except AttributeError: pass else: - raise Exception( - "get_authentication_events MUST return a list of AuthnEvent" - ) + raise Exception("get_authentication_events MUST return a list of AuthnEvent") # and now cove add_grant grant = self.session_manager[_session_id] grant_kwargs = grant.parameter - for i in ('not_before', 'used'): + for i in ("not_before", "used"): grant_kwargs.pop(i) - self.session_manager.add_grant("diana", "client_1", **grant_kwargs ) + self.session_manager.add_grant("diana", "client_1", **grant_kwargs) diff --git a/tests/test_06_session_manager_pairwise.py b/tests/test_06_session_manager_pairwise.py index 333c230b..c2c377fd 100644 --- a/tests/test_06_session_manager_pairwise.py +++ b/tests/test_06_session_manager_pairwise.py @@ -1,6 +1,4 @@ import os -import pytest - from oidcop.exception import ConfigurationError from oidcop.session.manager import PairWiseID @@ -12,42 +10,39 @@ class TestSessionManagerPairWiseID: def test_paiwise_id(self): # as param - pw = PairWiseID(salt='salt') - pw('diana', 'that-sector') + pw = PairWiseID(salt="salt") + pw("diana", "that-sector") # as file - pw = PairWiseID(filename='salt.txt') - pw('diana', 'that-sector') + pw = PairWiseID(filename="salt.txt") + pw("diana", "that-sector") # prune - os.remove('salt.txt') + os.remove("salt.txt") # again to test if a preexistent file going ot be used ... - pw = PairWiseID(filename='salt.txt') + pw = PairWiseID(filename="salt.txt") try: - pw = PairWiseID(filename='/tmp') + pw = PairWiseID(filename="/tmp") except ConfigurationError: - pass # that's ok + pass # that's ok # as random pw = PairWiseID() - pw('diana', 'that-sector') + pw("diana", "that-sector") self.cleanup() def cleanup(self): - if os.path.isfile('salt.txt'): - os.remove('salt.txt') + if os.path.isfile("salt.txt"): + os.remove("salt.txt") class TestSessionManagerPublicID: - pw = PublicID() - pw('diana', 'that-sector') + pw = PublicID() + pw("diana", "that-sector") class TestSessionManagerConf: - sman = SessionManager( - handler = TokenHandler(), - conf={'password': 'hola!'} - ) + sman = SessionManager(handler=TokenHandler(), conf={"password": "hola!"}) diff --git a/tests/test_07_userinfo.py b/tests/test_07_userinfo.py index 4ab0394e..d163e945 100644 --- a/tests/test_07_userinfo.py +++ b/tests/test_07_userinfo.py @@ -1,6 +1,7 @@ import json import os +from oidcop.configure import OPConfiguration import pytest from oidcmsg.oidc import OpenIDRequest @@ -93,9 +94,7 @@ def test_default_scope2claims(): "email", "email_verified", } - assert set(convert_scopes2claims(["address"], STANDARD_CLAIMS).keys()) == { - "address" - } + assert set(convert_scopes2claims(["address"], STANDARD_CLAIMS).keys()) == {"address"} assert set(convert_scopes2claims(["phone"], STANDARD_CLAIMS).keys()) == { "phone_number", "phone_number_verified", @@ -185,9 +184,7 @@ def create_endpoint_context(self): "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -216,18 +213,12 @@ def create_endpoint_context(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "{}/registration", - "class": Registration, - "kwargs": {}, - }, + "registration": {"path": "{}/registration", "class": Registration, "kwargs": {},}, "authorization": { "path": "{}/authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post",], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -238,16 +229,9 @@ def create_endpoint_context(self): "path": "userinfo", "class": userinfo.UserInfo, "kwargs": { - "claim_types_supported": [ - "normal", - "aggregated", - "distributed", - ], + "claim_types_supported": ["normal", "aggregated", "distributed",], "client_authn_method": ["bearer_header"], - "base_claims": { - "eduperson_scoped_affiliation": None, - "email": None, - }, + "base_claims": {"eduperson_scoped_affiliation": None, "email": None,}, "add_claims_by_scope": True, "enable_claims_per_client": True, }, @@ -268,7 +252,7 @@ def create_endpoint_context(self): "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) self.endpoint_context = server.endpoint_context # Just has to be there self.endpoint_context.cdb["client1"] = {} @@ -387,9 +371,7 @@ def test_collect_user_info_enable_claims_per_client(self): _userinfo_endpoint.kwargs["enable_claims_per_client"] = True del _userinfo_endpoint.kwargs["base_claims"] - self.endpoint_context.cdb[_req["client_id"]]["userinfo_claims"] = { - "phone_number": None - } + self.endpoint_context.cdb[_req["client_id"]]["userinfo_claims"] = {"phone_number": None} _userinfo_restriction = self.claims_interface.get_claims( session_id=session_id, scopes=_req["scope"], usage="userinfo" @@ -408,9 +390,8 @@ def create_endpoint_context(self): "userinfo": {"class": UserInfo, "kwargs": {"db": USERINFO_DB}}, "password": "we didn't start the fire", "issuer": "https://example.com/op", - "token_expires_in": 900, - "grant_expires_in": 600, - "refresh_token_expires_in": 86400, + "claims_interface": {"class": "oidcop.session.claims.OAuth2ClaimsInterface", + "kwargs": {}}, "endpoint": { "provider_config": { "path": "{}/.well-known/openid-configuration", @@ -429,11 +410,7 @@ def create_endpoint_context(self): "response_types_supported": [ " ".join(x) for x in RESPONSE_TYPES_SUPPORTED ], - "response_modes_supported": [ - "query", - "fragment", - "form_post", - ], + "response_modes_supported": ["query", "fragment", "form_post",], "claims_parameter_supported": True, "request_parameter_supported": True, "request_uri_parameter_supported": True, @@ -443,16 +420,9 @@ def create_endpoint_context(self): "path": "userinfo", "class": userinfo.UserInfo, "kwargs": { - "claim_types_supported": [ - "normal", - "aggregated", - "distributed", - ], + "claim_types_supported": ["normal", "aggregated", "distributed",], "client_authn_method": ["bearer_header"], - "base_claims": { - "eduperson_scoped_affiliation": None, - "email": None, - }, + "base_claims": {"eduperson_scoped_affiliation": None, "email": None,}, "add_claims_by_scope": True, "enable_claims_per_client": True, }, diff --git a/tests/test_08_session_life.py b/tests/test_08_session_life.py index 4e759098..9aca9394 100644 --- a/tests/test_08_session_life.py +++ b/tests/test_08_session_life.py @@ -1,5 +1,6 @@ import os +from oidcop.configure import OPConfiguration import pytest from cryptojwt.key_jar import init_key_jar from oidcmsg.oidc import AccessTokenRequest @@ -50,7 +51,8 @@ def setup_token_handler(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + self.endpoint_context = server.endpoint_context self.session_manager = self.endpoint_context.session_manager @@ -92,9 +94,7 @@ def auth(self): # the grant is assigned to a session (user_id, client_id) self.session_manager.set([user_id, client_id, grant.id], grant) - session_id = self.session_manager.encrypted_session_id( - user_id, client_id, grant.id - ) + session_id = self.session_manager.encrypted_session_id(user_id, client_id, grant.id) # Constructing an authorization code is now done by @@ -107,9 +107,7 @@ def auth(self): ) # get user info - user_info = self.session_manager.get_user_info( - uid = user_id, - ) + user_info = self.session_manager.get_user_info(uid=user_id,) return grant.id, code def test_code_flow(self): @@ -179,9 +177,7 @@ def test_code_flow(self): scope=["openid", "mail", "offline_access"], ) - reftok = self.session_manager.find_token( - session_id, REFRESH_TOKEN_REQ["refresh_token"] - ) + reftok = self.session_manager.find_token(session_id, REFRESH_TOKEN_REQ["refresh_token"]) assert reftok.supports_minting("access_token") @@ -280,11 +276,7 @@ def setup_session_manager(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "{}/registration", - "class": Registration, - "kwargs": {}, - }, + "registration": {"path": "{}/registration", "class": Registration, "kwargs": {},}, "authorization": { "path": "{}/authorization", "class": Authorization, @@ -307,7 +299,7 @@ def setup_session_manager(self): "kwargs": {"db_file": full_path("users.json")}, }, } - server = Server(conf, keyjar=KEYJAR) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), keyjar=KEYJAR, cwd=BASEDIR) self.endpoint_context = server.endpoint_context self.session_manager = self.endpoint_context.session_manager # self.session_manager = SessionManager(handler=self.endpoint_context.sdb.handler) @@ -353,9 +345,7 @@ def auth(self): # the grant is assigned to a session (user_id, client_id) self.session_manager.set([user_id, client_id, grant.id], grant) - session_id = self.session_manager.encrypted_session_id( - user_id, client_id, grant.id - ) + session_id = self.session_manager.encrypted_session_id(user_id, client_id, grant.id) # Constructing an authorization code is now done by code = grant.mint_token( session_id=session_id, @@ -383,9 +373,7 @@ def test_code_flow(self): # parse the token session_id = self.session_manager.token_handler.sid(TOKEN_REQ["code"]) - user_id, client_id, grant_id = self.session_manager.decrypt_session_id( - session_id - ) + user_id, client_id, grant_id = self.session_manager.decrypt_session_id(session_id) # Now given I have the client_id from the request and the user_id from the # token I can easily find the grant @@ -444,9 +432,7 @@ def test_code_flow(self): session_id = self.session_manager.encrypted_session_id( user_id, REFRESH_TOKEN_REQ["client_id"], grant_id ) - reftok = self.session_manager.find_token( - session_id, REFRESH_TOKEN_REQ["refresh_token"] - ) + reftok = self.session_manager.find_token(session_id, REFRESH_TOKEN_REQ["refresh_token"]) # Can I use this token to mint another token ? assert grant.is_active() diff --git a/tests/test_09_cookie_handler.py b/tests/test_09_cookie_handler.py index 5b120c9c..b170faf2 100644 --- a/tests/test_09_cookie_handler.py +++ b/tests/test_09_cookie_handler.py @@ -37,9 +37,7 @@ def test_make_cookie_content_max_age(self): assert len(_cookie_info["value"].split("|")) == 3 def test_read_cookie_info(self): - _cookie_info = [ - self.cookie_handler.make_cookie_content("oidcop", "value", "sso") - ] + _cookie_info = [self.cookie_handler.make_cookie_content("oidcop", "value", "sso")] returned = [{"name": c["name"], "value": c["value"]} for c in _cookie_info] _info = self.cookie_handler.parse_cookie("oidcop", returned) assert len(_info) == 1 @@ -50,9 +48,7 @@ def test_read_cookie_info(self): def test_mult_cookie(self): _cookie = [ self.cookie_handler.make_cookie_content("oidcop", "value", "sso"), - self.cookie_handler.make_cookie_content( - "oidcop", "session_state", "session" - ), + self.cookie_handler.make_cookie_content("oidcop", "session_state", "session"), ] assert len(_cookie) == 2 _c_info = self.cookie_handler.parse_cookie("oidcop", _cookie) @@ -88,9 +84,7 @@ def test_make_cookie_content_max_age(self): assert len(_cookie_info["value"].split("|")) == 4 def test_read_cookie_info(self): - _cookie_info = [ - self.cookie_handler.make_cookie_content("oidcop", "value", "sso") - ] + _cookie_info = [self.cookie_handler.make_cookie_content("oidcop", "value", "sso")] returned = [{"name": c["name"], "value": c["value"]} for c in _cookie_info] _info = self.cookie_handler.parse_cookie("oidcop", returned) assert len(_info) == 1 @@ -101,9 +95,7 @@ def test_read_cookie_info(self): def test_mult_cookie(self): _cookie = [ self.cookie_handler.make_cookie_content("oidcop", "value", "sso"), - self.cookie_handler.make_cookie_content( - "oidcop", "session_state", "session" - ), + self.cookie_handler.make_cookie_content("oidcop", "session_state", "session"), ] assert len(_cookie) == 2 _c_info = self.cookie_handler.parse_cookie("oidcop", _cookie) @@ -138,9 +130,7 @@ def test_make_cookie_content_max_age(self): assert len(_cookie_info["value"].split("|")) == 4 def test_read_cookie_info(self): - _cookie_info = [ - self.cookie_handler.make_cookie_content("oidcop", "value", "sso") - ] + _cookie_info = [self.cookie_handler.make_cookie_content("oidcop", "value", "sso")] returned = [{"name": c["name"], "value": c["value"]} for c in _cookie_info] _info = self.cookie_handler.parse_cookie("oidcop", returned) assert len(_info) == 1 @@ -151,9 +141,7 @@ def test_read_cookie_info(self): def test_mult_cookie(self): _cookie = [ self.cookie_handler.make_cookie_content("oidcop", "value", "sso"), - self.cookie_handler.make_cookie_content( - "oidcop", "session_state", "session" - ), + self.cookie_handler.make_cookie_content("oidcop", "session_state", "session"), ] assert len(_cookie) == 2 _c_info = self.cookie_handler.parse_cookie("oidcop", _cookie) @@ -192,9 +180,7 @@ def test_make_cookie_content_max_age(self): assert len(_cookie_info["value"].split("|")) == 4 def test_read_cookie_info(self): - _cookie_info = [ - self.cookie_handler.make_cookie_content("oidcop", "value", "sso") - ] + _cookie_info = [self.cookie_handler.make_cookie_content("oidcop", "value", "sso")] returned = [{"name": c["name"], "value": c["value"]} for c in _cookie_info] _info = self.cookie_handler.parse_cookie("oidcop", returned) assert len(_info) == 1 @@ -205,9 +191,7 @@ def test_read_cookie_info(self): def test_mult_cookie(self): _cookie = [ self.cookie_handler.make_cookie_content("oidcop", "value", "sso"), - self.cookie_handler.make_cookie_content( - "oidcop", "session_state", "session" - ), + self.cookie_handler.make_cookie_content("oidcop", "session_state", "session"), ] assert len(_cookie) == 2 _c_info = self.cookie_handler.parse_cookie("oidcop", _cookie) @@ -219,9 +203,7 @@ def test_mult_cookie(self): def test_compute_session_state(): - hv = compute_session_state( - "state", "salt", "client_id", "https://example.com/redirect" - ) + hv = compute_session_state("state", "salt", "client_id", "https://example.com/redirect") assert hv == "d21113fbe4b54661ae45f3a3233b0f865ccc646af248274b6fa5664267540e29.salt" diff --git a/tests/test_12_user_authn.py b/tests/test_12_user_authn.py index e0fe2cf2..37e88374 100644 --- a/tests/test_12_user_authn.py +++ b/tests/test_12_user_authn.py @@ -3,17 +3,16 @@ import pytest +from oidcop.configure import OPConfiguration from oidcop.server import Server from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD from oidcop.user_authn.authn_context import UNSPECIFIED -from oidcop.user_authn.user import NoAuthn -from oidcop.user_authn.user import UserPassJinja2 from oidcop.user_authn.user import BasicAuthn from oidcop.user_authn.user import NoAuthn from oidcop.user_authn.user import SymKeyAuthn +from oidcop.user_authn.user import UserPassJinja2 from oidcop.util import JSONDictDB - KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, {"type": "EC", "crv": "P-256", "use": ["sig"]}, @@ -54,12 +53,9 @@ def create_endpoint_context(self): "passwd_label": "Secret sauce", }, }, - "anon": { - "acr": UNSPECIFIED, - "class": NoAuthn, - "kwargs": {"user": "diana"}, - }, + "anon": {"acr": UNSPECIFIED, "class": NoAuthn, "kwargs": {"user": "diana"}, }, }, + "template_dir": "templates", "cookie_handler": { "class": "oidcop.cookie_handler.CookieHandler", "kwargs": { @@ -71,10 +67,9 @@ def create_endpoint_context(self): }, }, }, - "template_dir": "tests/templates", } - server = Server(conf) - self.endpoint_context = server.endpoint_context + self.server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + self.endpoint_context = self.server.endpoint_context def test_authenticated_as_without_cookie(self): authn_item = self.endpoint_context.authn_broker.pick(INTERNETPROTOCOLPASSWORD) @@ -104,29 +99,23 @@ def test_authenticated_as_with_cookie(self): def test_userpassjinja2(self): db = { - "class": JSONDictDB, - "kwargs": {"filename": full_path("passwd.json")}, + "class": JSONDictDB, + "kwargs": {"filename": full_path("passwd.json")}, } template_handler = self.endpoint_context.template_handler - sg = self.endpoint_context.session_manager.token_handler.handler['access_token'].kwargs['server_get'] - res = UserPassJinja2(db, template_handler, server_get=sg) + res = UserPassJinja2(db, template_handler, + server_get=self.server.server_get) res() - assert 'page_header' in res.kwargs - + assert "page_header" in res.kwargs def test_basic_auth(self): - sg = self.endpoint_context.session_manager.token_handler.handler['access_token'].kwargs['server_get'] - basic_auth = base64.b64encode(b'diana:krall').decode() - ba = BasicAuthn(pwd={'diana': 'krall'}, - server_get=sg) - ba.authenticated_as( - client_id='', authorization=f"Basic {basic_auth}" - ) + basic_auth = base64.b64encode(b"diana:krall").decode() + ba = BasicAuthn(pwd={"diana": "krall"}, server_get=self.server.server_get) + ba.authenticated_as(client_id="", authorization=f"Basic {basic_auth}") def test_no_auth(self): - sg = self.endpoint_context.session_manager.token_handler.handler['access_token'].kwargs['server_get'] basic_auth = base64.b64encode( - b'D\xfd\x8a\x85\xa6\xd1\x16\xe4\\6\x1e\x9ds~\xc3\t\x95\x99\x83\x91\x1f\xfb:iviviviv' + b"D\xfd\x8a\x85\xa6\xd1\x16\xe4\\6\x1e\x9ds~\xc3\t\x95\x99\x83\x91\x1f\xfb:iviviviv" ) - ba = SymKeyAuthn(symkey=b'0'*32, ttl=600, server_get=sg) - ba.authenticated_as(client_id='', authorization=basic_auth) + ba = SymKeyAuthn(symkey=b"0" * 32, ttl=600, server_get=self.server.server_get) + ba.authenticated_as(client_id="", authorization=basic_auth) diff --git a/tests/test_13_login_hint.py b/tests/test_13_login_hint.py index 025a572d..7213a27a 100644 --- a/tests/test_13_login_hint.py +++ b/tests/test_13_login_hint.py @@ -2,7 +2,6 @@ import os from oidcop.configure import OPConfiguration -from oidcop.configure import create_from_config_file from oidcop.endpoint_context import init_service from oidcop.endpoint_context import init_user_info from oidcop.login_hint import LoginHint2Acrs @@ -17,15 +16,9 @@ def full_path(local_file): def test_login_hint(): userinfo = init_user_info( - { - "class": "oidcop.user_info.UserInfo", - "kwargs": {"db_file": full_path("users.json")}, - }, - "", - ) - login_hint_lookup = init_service( - {"class": "oidcop.login_hint.LoginHintLookup"}, None + {"class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": full_path("users.json")},}, "", ) + login_hint_lookup = init_service({"class": "oidcop.login_hint.LoginHintLookup"}, None) login_hint_lookup.userinfo = userinfo assert login_hint_lookup("tel:0907865000") == "diana" @@ -46,9 +39,7 @@ def test_login_hint2acrs_unmatched_schema(): def test_server_login_hint_lookup(): _str = open(full_path("op_config.json")).read() _conf = json.loads(_str) - configuration = OPConfiguration( - conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf=_conf, base_path=BASEDIR, domain="127.0.0.1", port=443) server = Server(configuration) assert server.endpoint_context.login_hint_lookup("tel:0907865000") == "diana" diff --git a/tests/test_20_endpoint.py b/tests/test_20_endpoint.py index 82373e7f..91e37a99 100755 --- a/tests/test_20_endpoint.py +++ b/tests/test_20_endpoint.py @@ -1,6 +1,8 @@ import json +import os from urllib.parse import urlparse +from oidcop.configure import OPConfiguration import pytest from oidcmsg.message import Message @@ -8,6 +10,8 @@ from oidcop.server import Server from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, {"type": "EC", "crv": "P-256", "use": ["sig"]}, @@ -44,9 +48,7 @@ def create_endpoint(self): "grant_expires_in": 300, "refresh_token_expires_in": 86400, "verify_ssl": False, - "endpoint": { - "endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}}, - }, + "endpoint": {"endpoint": {"path": "endpoint", "class": Endpoint, "kwargs": {}},}, "keys": { "public_path": "jwks.json", "key_defs": KEYDEFS, @@ -62,7 +64,8 @@ def create_endpoint(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + self.endpoint_context = server.endpoint_context self.endpoint = server.server_get("endpoint", "") @@ -166,9 +169,7 @@ def test_do_response_response_msg_1(self): assert ("Content-type", "application/json") in info["http_headers"] self.endpoint.response_format = "jws" - info = self.endpoint.do_response( - EXAMPLE_MSG, response_msg="header.payload.sign" - ) + info = self.endpoint.do_response(EXAMPLE_MSG, response_msg="header.payload.sign") assert info["response"] == "header.payload.sign" assert ("Content-type", "application/jose") in info["http_headers"] @@ -177,9 +178,7 @@ def test_do_response_response_msg_1(self): info = self.endpoint.do_response(EXAMPLE_MSG, response_msg="foo=bar") assert info["response"] == "foo=bar" - assert ("Content-type", "application/x-www-form-urlencoded") in info[ - "http_headers" - ] + assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] info = self.endpoint.do_response( EXAMPLE_MSG, response_msg="{foo=bar}", content_type="application/json" @@ -188,9 +187,7 @@ def test_do_response_response_msg_1(self): assert ("Content-type", "application/json") in info["http_headers"] info = self.endpoint.do_response( - EXAMPLE_MSG, - response_msg="header.payload.sign", - content_type="application/jose", + EXAMPLE_MSG, response_msg="header.payload.sign", content_type="application/jose", ) assert info["response"] == "header.payload.sign" assert ("Content-type", "application/jose") in info["http_headers"] @@ -198,22 +195,15 @@ def test_do_response_response_msg_1(self): def test_do_response_placement_body(self): self.endpoint.response_placement = "body" info = self.endpoint.do_response(EXAMPLE_MSG) - assert ("Content-type", "application/json; charset=utf-8") in info[ - "http_headers" - ] + assert ("Content-type", "application/json; charset=utf-8") in info["http_headers"] assert ( - info["response"] - == '{"name": "Doe, Jane", "given_name": "Jane", "family_name": "Doe"}' + info["response"] == '{"name": "Doe, Jane", "given_name": "Jane", "family_name": "Doe"}' ) def test_do_response_placement_url(self): self.endpoint.response_placement = "url" - info = self.endpoint.do_response( - EXAMPLE_MSG, return_uri="https://example.org/cb" - ) - assert ("Content-type", "application/x-www-form-urlencoded") in info[ - "http_headers" - ] + info = self.endpoint.do_response(EXAMPLE_MSG, return_uri="https://example.org/cb") + assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] assert ( info["response"] == "https://example.org/cb?name=Doe%2C+Jane&given_name=Jane&family_name=Doe" @@ -222,9 +212,7 @@ def test_do_response_placement_url(self): info = self.endpoint.do_response( EXAMPLE_MSG, return_uri="https://example.org/cb", fragment_enc=True ) - assert ("Content-type", "application/x-www-form-urlencoded") in info[ - "http_headers" - ] + assert ("Content-type", "application/x-www-form-urlencoded") in info["http_headers"] assert ( info["response"] == "https://example.org/cb#name=Doe%2C+Jane&given_name=Jane&family_name=Doe" diff --git a/tests/test_21_oidc_discovery_endpoint.py b/tests/test_21_oidc_discovery_endpoint.py index 24f7b38a..448d16e5 100755 --- a/tests/test_21_oidc_discovery_endpoint.py +++ b/tests/test_21_oidc_discovery_endpoint.py @@ -1,5 +1,7 @@ import json +import os +from oidcop.configure import OPConfiguration import pytest from oidcop.oidc.discovery import Discovery @@ -11,6 +13,8 @@ {"type": "EC", "crv": "P-256", "use": ["sig"]}, ] +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + class TestEndpoint(object): @pytest.fixture(autouse=True) @@ -39,7 +43,7 @@ def create_endpoint(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) self.endpoint = server.server_get("endpoint", "discovery") def test_do_response(self): diff --git a/tests/test_22_oidc_provider_config_endpoint.py b/tests/test_22_oidc_provider_config_endpoint.py index 5a393a31..df950023 100755 --- a/tests/test_22_oidc_provider_config_endpoint.py +++ b/tests/test_22_oidc_provider_config_endpoint.py @@ -1,11 +1,15 @@ import json +import os +from oidcop.configure import OPConfiguration import pytest from oidcop.oidc.provider_config import ProviderConfiguration from oidcop.oidc.token import Token from oidcop.server import Server +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, {"type": "EC", "crv": "P-256", "use": ["sig"]}, @@ -64,7 +68,8 @@ def create_endpoint(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + self.endpoint_context = server.endpoint_context self.endpoint = server.server_get("endpoint", "provider_config") @@ -98,6 +103,4 @@ def test_do_response(self): "updated_at", "birthdate", } - assert ("Content-type", "application/json; charset=utf-8") in msg[ - "http_headers" - ] + assert ("Content-type", "application/json; charset=utf-8") in msg["http_headers"] diff --git a/tests/test_23_oidc_registration_endpoint.py b/tests/test_23_oidc_registration_endpoint.py index ceb3e7db..eb2c23f2 100755 --- a/tests/test_23_oidc_registration_endpoint.py +++ b/tests/test_23_oidc_registration_endpoint.py @@ -1,6 +1,8 @@ # -*- coding: latin-1 -*- import json +import os +from oidcop.configure import OPConfiguration import pytest import responses from cryptojwt.key_jar import init_key_jar @@ -16,6 +18,8 @@ from oidcop.oidc.userinfo import UserInfo from oidcop.server import Server +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, {"type": "EC", "crv": "P-256", "use": ["sig"]}, @@ -87,9 +91,7 @@ def create_endpoint(self): "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -129,15 +131,9 @@ def create_endpoint(self): "path": "authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], - "claim_types_supported": [ - "normal", - "aggregated", - "distributed", - ], + "claim_types_supported": ["normal", "aggregated", "distributed",], "claims_parameter_supported": True, "request_parameter_supported": True, "request_uri_parameter_supported": True, @@ -159,7 +155,7 @@ def create_endpoint(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) self.endpoint = server.server_get("endpoint", "registration") def test_parse(self): diff --git a/tests/test_24_oauth2_authorization_endpoint.py b/tests/test_24_oauth2_authorization_endpoint.py index fbdb52b9..319a9a15 100755 --- a/tests/test_24_oauth2_authorization_endpoint.py +++ b/tests/test_24_oauth2_authorization_endpoint.py @@ -5,6 +5,7 @@ from urllib.parse import parse_qs from urllib.parse import urlparse +from oidcop.configure import ASConfiguration import pytest import yaml from cryptojwt import KeyJar @@ -151,9 +152,7 @@ def create_endpoint(self): "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -183,9 +182,7 @@ def create_endpoint(self): "path": "{}/authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -219,20 +216,12 @@ def create_endpoint(self): "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", - "id_token", - ], + "supports_minting": ["access_token", "refresh_token", "id_token",], }, }, "expires_in": 43200, @@ -240,7 +229,8 @@ def create_endpoint(self): }, }, } - server = Server(conf) + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) endpoint_context.cdb = _clients["clients"] @@ -316,9 +306,7 @@ def test_do_response_code_token(self): def test_verify_uri_unknown_client(self): request = {"redirect_uri": "https://rp.example.com/cb"} with pytest.raises(UnknownClient): - verify_uri( - self.endpoint.server_get("endpoint_context"), request, "redirect_uri" - ) + verify_uri(self.endpoint.server_get("endpoint_context"), request, "redirect_uri") def test_verify_uri_fragment(self): _context = self.endpoint.server_get("endpoint_context") @@ -336,9 +324,7 @@ def test_verify_uri_noregistered(self): def test_verify_uri_unregistered(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/auth_cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/auth_cb", {})]} request = {"redirect_uri": "https://rp.example.com/cb"} @@ -380,9 +366,7 @@ def test_verify_uri_qp_mismatch(self): def test_verify_uri_qp_missing(self): _context = self.endpoint.server_get("endpoint_context") _context.cdb["client_id"] = { - "redirect_uris": [ - ("https://rp.example.com/cb", {"foo": ["bar"], "state": ["low"]}) - ] + "redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"], "state": ["low"]})] } request = {"redirect_uri": "https://rp.example.com/cb?foo=bar"} @@ -401,9 +385,7 @@ def test_verify_uri_qp_missing_val(self): def test_verify_uri_no_registered_qp(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {})]} request = {"redirect_uri": "https://rp.example.com/cb?foo=bob"} with pytest.raises(ValueError): @@ -411,9 +393,7 @@ def test_verify_uri_no_registered_qp(self): def test_verify_uri_wrong_uri_type(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {})]} request = {"redirect_uri": "https://rp.example.com/cb?foo=bob"} with pytest.raises(ValueError): @@ -431,9 +411,7 @@ def test_verify_uri_none_registered(self): def test_get_uri(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {})]} request = { "redirect_uri": "https://rp.example.com/cb", @@ -444,9 +422,7 @@ def test_get_uri(self): def test_get_uri_no_redirect_uri(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {})]} request = {"client_id": "client_id"} @@ -454,9 +430,7 @@ def test_get_uri_no_redirect_uri(self): def test_get_uri_no_registered(self): _context = self.endpoint.server_get("endpoint_context") - _context.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {})] - } + _context.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {})]} request = {"client_id": "client_id"} @@ -514,9 +488,9 @@ def test_setup_auth(self): "id_token_signed_response_alg": "RS256", } - kaka = self.endpoint.server_get( - "endpoint_context" - ).cookie_handler.make_cookie_content("value", "sso") + kaka = self.endpoint.server_get("endpoint_context").cookie_handler.make_cookie_content( + "value", "sso" + ) res = self.endpoint.setup_auth(request, redirect_uri, cinfo, [kaka]) assert set(res.keys()) == {"session_id", "identity", "user"} @@ -575,9 +549,7 @@ def test_setup_auth_invalid_scope(self): _context.conf["capabilities"]["deny_unknown_scopes"] = True excp = None try: - res = self.endpoint.process_request( - request, http_info={"headers": {"cookie": [kaka]}} - ) + res = self.endpoint.process_request(request, http_info={"headers": {"cookie": [kaka]}}) except UnAuthorizedClientScope as e: excp = e assert excp @@ -602,9 +574,7 @@ def test_setup_auth_user(self): session_id = self._create_session(request) item = self.endpoint.server_get("endpoint_context").authn_broker.db["anon"] - item["method"].user = b64e( - as_bytes(json.dumps({"uid": "krall", "sid": session_id})) - ) + item["method"].user = b64e(as_bytes(json.dumps({"uid": "krall", "sid": session_id}))) res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None) assert set(res.keys()) == {"session_id", "identity", "user"} @@ -634,9 +604,7 @@ def test_setup_auth_session_revoked(self): _csi.revoked = True item = _context.authn_broker.db["anon"] - item["method"].user = b64e( - as_bytes(json.dumps({"uid": "krall", "sid": session_id})) - ) + item["method"].user = b64e(as_bytes(json.dumps({"uid": "krall", "sid": session_id}))) res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None) assert set(res.keys()) == {"args", "function"} @@ -654,8 +622,7 @@ def test_response_mode_form_post(self): "response_placement", } assert info["response_msg"] == FORM_POST.format( - action="https://example.com/cb", - inputs='', + action="https://example.com/cb", inputs='', ) def test_response_mode_fragment(self): @@ -683,9 +650,7 @@ def test_req_user(self): "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "RS256", } - res = self.endpoint.setup_auth( - request, redirect_uri, cinfo, None, req_user="adam" - ) + res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None, req_user="adam") assert "function" in res def test_req_user_no_prompt(self): @@ -704,9 +669,7 @@ def test_req_user_no_prompt(self): "redirect_uris": [("https://rp.example.com/cb", {})], "id_token_signed_response_alg": "RS256", } - res = self.endpoint.setup_auth( - request, redirect_uri, cinfo, None, req_user="adam" - ) + res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None, req_user="adam") assert "error" in res # def test_sso(self): diff --git a/tests/test_24_oauth2_authorization_endpoint_jar.py b/tests/test_24_oauth2_authorization_endpoint_jar.py index 0d0c629f..ad4f4523 100755 --- a/tests/test_24_oauth2_authorization_endpoint_jar.py +++ b/tests/test_24_oauth2_authorization_endpoint_jar.py @@ -3,6 +3,7 @@ import os from http.cookies import SimpleCookie +from oidcop.configure import ASConfiguration import pytest import responses import yaml @@ -129,9 +130,7 @@ def create_endpoint(self): "path": "{}/authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -161,7 +160,7 @@ def create_endpoint(self): }, }, } - server = Server(conf) + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) endpoint_context.cdb = _clients["clients"] @@ -179,8 +178,7 @@ def create_endpoint(self): def test_parse_request_parameter(self): _jwt = JWT(key_jar=self.rp_keyjar, iss="client_1", sign_alg="HS256") _jws = _jwt.pack( - AUTH_REQ_DICT, - aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], + AUTH_REQ_DICT, aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], ) # ----------------- _req = self.endpoint.parse_request( @@ -197,8 +195,7 @@ def test_parse_request_parameter(self): def test_parse_request_uri(self): _jwt = JWT(key_jar=self.rp_keyjar, iss="client_1", sign_alg="HS256") _jws = _jwt.pack( - AUTH_REQ_DICT, - aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], + AUTH_REQ_DICT, aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], ) request_uri = "https://client.example.com/req" diff --git a/tests/test_24_oauth2_token_endpoint.py b/tests/test_24_oauth2_token_endpoint.py new file mode 100644 index 00000000..2222447d --- /dev/null +++ b/tests/test_24_oauth2_token_endpoint.py @@ -0,0 +1,480 @@ +import json +import os + +from cryptojwt import JWT +from cryptojwt.key_jar import build_keyjar +from oidcmsg.oidc import AccessTokenRequest +from oidcmsg.oidc import AuthorizationRequest +from oidcmsg.oidc import RefreshAccessTokenRequest +from oidcmsg.oidc import TokenErrorResponse +from oidcmsg.time_util import utc_time_sans_frac +import pytest + +from oidcop import JWT_BEARER +from oidcop.authn_event import create_authn_event +from oidcop.authz import AuthzHandling +from oidcop.client_authn import verify_client +from oidcop.configure import ASConfiguration +from oidcop.exception import UnAuthorizedClient +from oidcop.oauth2.authorization import Authorization +from oidcop.oauth2.token import Token +from oidcop.server import Server +from oidcop.session import MintingNotAllowed +from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD +from oidcop.user_info import UserInfo + +KEYDEFS = [ + {"type": "RSA", "key": "", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, +] + +CLIENT_KEYJAR = build_keyjar(KEYDEFS) + +RESPONSE_TYPES_SUPPORTED = [ + ["code"], + ["token"], +] + +CAPABILITIES = { + "grant_types_supported": [ + "authorization_code", + "implicit", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "refresh_token", + ], +} + +AUTH_REQ = AuthorizationRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + scope=["openid"], + state="STATE", + response_type="code", +) + +TOKEN_REQ = AccessTokenRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + state="STATE", + grant_type="authorization_code", + client_secret="hemligt", +) + +REFRESH_TOKEN_REQ = RefreshAccessTokenRequest( + grant_type="refresh_token", client_id="client_1", client_secret="hemligt" +) + +TOKEN_REQ_DICT = TOKEN_REQ.to_dict() + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def full_path(local_file): + return os.path.join(BASEDIR, local_file) + + +USERINFO = UserInfo(json.loads(open(full_path("users.json")).read())) + + +@pytest.fixture +def conf(): + return { + "issuer": "https://example.com/", + "password": "mycket hemligt", + "verify_ssl": False, + "capabilities": CAPABILITIES, + "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, + "token_handler_args": { + "jwks_file": "private/token_jwks.json", + "code": {"kwargs": {"lifetime": 600}}, + "token": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "add_claims_by_scope": True, + "aud": ["https://example.org/appl"], + }, + }, + "refresh": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"],}, + }, + }, + "endpoint": { + "authorization": {"path": "authorization", "class": Authorization, "kwargs": {},}, + "token": { + "path": "token", + "class": Token, + "kwargs": { + "client_authn_method": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + ] + }, + }, + }, + "authentication": { + "anon": { + "acr": INTERNETPROTOCOLPASSWORD, + "class": "oidcop.user_authn.user.NoAuthn", + "kwargs": {"user": "diana"}, + } + }, + "userinfo": {"class": UserInfo, "kwargs": {"db": {}}}, + "client_authn": verify_client, + "template_dir": "template", + "claims_interface": {"class": "oidcop.session.claims.OAuth2ClaimsInterface", "kwargs": {}}, + "authz": { + "class": AuthzHandling, + "kwargs": { + "grant_config": { + "usage_rules": { + "authorization_code": { + "expires_in": 300, + "supports_minting": ["access_token", "refresh_token"], + "max_usage": 1, + }, + "access_token": {"expires_in": 600}, + "refresh_token": { + "expires_in": 86400, + "supports_minting": ["access_token", "refresh_token"], + }, + }, + "expires_in": 43200, + } + }, + }, + } + + +class TestEndpoint(object): + @pytest.fixture(autouse=True) + def create_endpoint(self, conf): + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context + endpoint_context.cdb["client_1"] = { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + } + endpoint_context.keyjar.import_jwks(CLIENT_KEYJAR.export_jwks(), "client_1") + self.session_manager = endpoint_context.session_manager + self.token_endpoint = server.server_get("endpoint", "token") + self.user_id = "diana" + self.endpoint_context = endpoint_context + + def test_init(self): + assert self.token_endpoint + + def _create_session(self, auth_req, sub_type="public", sector_identifier=""): + if sector_identifier: + authz_req = auth_req.copy() + authz_req["sector_identifier_uri"] = sector_identifier + else: + authz_req = auth_req + client_id = authz_req["client_id"] + ae = create_authn_event(self.user_id) + return self.session_manager.create_session( + ae, authz_req, self.user_id, client_id=client_id, sub_type=sub_type + ) + + def _mint_code(self, grant, client_id): + session_id = self.session_manager.encrypted_session_id(self.user_id, client_id, grant.id) + usage_rules = grant.usage_rules.get("authorization_code", {}) + _exp_in = usage_rules.get("expires_in") + + # Constructing an authorization code is now done + _code = grant.mint_token( + session_id=session_id, + endpoint_context=self.endpoint_context, + token_type="authorization_code", + token_handler=self.session_manager.token_handler["code"], + usage_rules=usage_rules, + ) + + if _exp_in: + if isinstance(_exp_in, str): + _exp_in = int(_exp_in) + if _exp_in: + _code.expires_at = utc_time_sans_frac() + _exp_in + return _code + + def _mint_access_token(self, grant, session_id, token_ref=None): + _session_info = self.session_manager.get_session_info(session_id) + usage_rules = grant.usage_rules.get("access_token", {}) + _exp_in = usage_rules.get("expires_in", 0) + + _token = grant.mint_token( + _session_info, + endpoint_context=self.endpoint_context, + token_type="access_token", + token_handler=self.session_manager.token_handler["access_token"], + based_on=token_ref, # Means the token (tok) was used to mint this token + usage_rules=usage_rules, + ) + if isinstance(_exp_in, str): + _exp_in = int(_exp_in) + if _exp_in: + _token.expires_at = utc_time_sans_frac() + _exp_in + + return _token + + def test_parse(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + assert set(_req.keys()) == set(_token_request.keys()) + + def test_process_request(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _context = self.endpoint_context + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + assert _resp + assert set(_resp.keys()) == {"cookie", "http_headers", "response_args"} + + def test_process_request_using_code_twice(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _context = self.endpoint_context + _token_request["code"] = code.value + + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used + _2nd_response = self.token_endpoint.parse_request(_token_request) + assert "error" in _2nd_response + + def test_do_response(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + + _resp = self.token_endpoint.process_request(request=_req) + msg = self.token_endpoint.do_response(request=_req, **_resp) + assert isinstance(msg, dict) + + def test_process_request_using_private_key_jwt(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + del _token_request["client_id"] + del _token_request["client_secret"] + _context = self.endpoint_context + + _jwt = JWT(CLIENT_KEYJAR, iss=AUTH_REQ["client_id"], sign_alg="RS256") + _jwt.with_jti = True + _assertion = _jwt.pack({"aud": [self.token_endpoint.full_path]}) + _token_request.update({"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}) + _token_request["code"] = code.value + + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req) + + # 2nd time used + with pytest.raises(UnAuthorizedClient): + self.token_endpoint.parse_request(_token_request) + + def test_do_refresh_access_token(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _cntx = self.endpoint_context + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = ["access_token", "refresh_token"] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + assert set(_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope", + } + msg = self.token_endpoint.do_response(request=_req, **_resp) + assert isinstance(msg, dict) + + def test_do_2nd_refresh_access_token(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _cntx = self.endpoint_context + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + + # Make sure ID Tokens can also be used by this refesh token + _token_value = _resp["response_args"]["refresh_token"] + _session_info = self.session_manager.get_session_info_by_token(_token_value) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) + _token.usage_rules["supports_minting"] = [ + "access_token", + "refresh_token", + "id_token", + ] + + _req = self.token_endpoint.parse_request(_request.to_json()) + _resp = self.token_endpoint.process_request(request=_req) + + _2nd_request = REFRESH_TOKEN_REQ.copy() + _2nd_request["refresh_token"] = _resp["response_args"]["refresh_token"] + _2nd_req = self.token_endpoint.parse_request(_request.to_json()) + _2nd_resp = self.token_endpoint.process_request(request=_req) + + assert set(_2nd_resp.keys()) == {"cookie", "response_args", "http_headers"} + assert set(_2nd_resp["response_args"].keys()) == { + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope", + } + msg = self.token_endpoint.do_response(request=_req, **_resp) + assert isinstance(msg, dict) + + def test_new_refresh_token(self, conf): + self.endpoint_context.cdb["client_1"] = { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + } + + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + assert "refresh_token" in _resp["response_args"] + first_refresh_token = _resp["response_args"]["refresh_token"] + + _refresh_request = REFRESH_TOKEN_REQ.copy() + _refresh_request["refresh_token"] = first_refresh_token + _2nd_req = self.token_endpoint.parse_request(_refresh_request.to_json()) + _2nd_resp = self.token_endpoint.process_request(request=_2nd_req, issue_refresh=True) + assert "refresh_token" in _2nd_resp["response_args"] + second_refresh_token = _2nd_resp["response_args"]["refresh_token"] + + _2d_refresh_request = REFRESH_TOKEN_REQ.copy() + _2d_refresh_request["refresh_token"] = second_refresh_token + _3rd_req = self.token_endpoint.parse_request(_2d_refresh_request.to_json()) + _3rd_resp = self.token_endpoint.process_request(request=_3rd_req, issue_refresh=True) + assert "access_token" in _3rd_resp["response_args"] + assert "refresh_token" in _3rd_resp["response_args"] + + assert first_refresh_token != second_refresh_token + + def test_do_refresh_access_token_not_allowed(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid", "offline_access"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _cntx = self.token_endpoint.server_get("endpoint_context") + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + # This is weird, issuing a refresh token that can't be used to mint anything + # but it's testing so anything goes. + grant.usage_rules["refresh_token"] = {"supports_minting": []} + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _resp["response_args"]["refresh_token"] + _req = self.token_endpoint.parse_request(_request.to_json()) + with pytest.raises(MintingNotAllowed): + self.token_endpoint.process_request(_req) + + def test_do_refresh_access_token_revoked(self): + areq = AUTH_REQ.copy() + areq["scope"] = ["openid"] + + session_id = self._create_session(areq) + grant = self.endpoint_context.authz(session_id, areq) + code = self._mint_code(grant, areq["client_id"]) + + _cntx = self.token_endpoint.server_get("endpoint_context") + + _token_request = TOKEN_REQ_DICT.copy() + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request) + _resp = self.token_endpoint.process_request(request=_req, issue_refresh=True) + + _refresh_token = _resp["response_args"]["refresh_token"] + _cntx.session_manager.revoke_token(session_id, _refresh_token) + + _request = REFRESH_TOKEN_REQ.copy() + _request["refresh_token"] = _refresh_token + _req = self.token_endpoint.parse_request(_request.to_json()) + # A revoked token is caught already when parsing the query. + assert isinstance(_req, TokenErrorResponse) + + def test_configure_grant_types(self): + conf = {"access_token": {"class": "oidcop.oidc.token.AccessTokenHelper"}} + + self.token_endpoint.configure_grant_types(conf) + + assert len(self.token_endpoint.helper) == 1 + assert "access_token" in self.token_endpoint.helper + assert "refresh_token" not in self.token_endpoint.helper diff --git a/tests/test_24_oidc_authorization_endpoint.py b/tests/test_24_oidc_authorization_endpoint.py index 4df743ba..e75e0447 100755 --- a/tests/test_24_oidc_authorization_endpoint.py +++ b/tests/test_24_oidc_authorization_endpoint.py @@ -4,6 +4,7 @@ from urllib.parse import parse_qs from urllib.parse import urlparse +from oidcop.configure import OPConfiguration import pytest import responses import yaml @@ -183,18 +184,12 @@ def create_endpoint(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "{}/registration", - "class": Registration, - "kwargs": {}, - }, + "registration": {"path": "{}/registration", "class": Registration, "kwargs": {},}, "authorization": { "path": "{}/authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -218,11 +213,7 @@ def create_endpoint(self): "class": userinfo.UserInfo, "kwargs": { "db_file": "users.json", - "claim_types_supported": [ - "normal", - "aggregated", - "distributed", - ], + "claim_types_supported": ["normal", "aggregated", "distributed",], }, }, }, @@ -241,11 +232,7 @@ def create_endpoint(self): "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": {}, @@ -273,7 +260,8 @@ def create_endpoint(self): "kwargs": {"scheme_map": {"email": [INTERNETPROTOCOLPASSWORD]}}, }, } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) @@ -416,8 +404,7 @@ def test_id_token_claims(self): _pr_resp = self.endpoint.parse_request(_req) _resp = self.endpoint.process_request(_pr_resp) idt = verify_id_token( - _resp["response_args"], - keyjar=self.endpoint.server_get("endpoint_context").keyjar, + _resp["response_args"], keyjar=self.endpoint.server_get("endpoint_context").keyjar, ) assert idt # from config @@ -441,8 +428,7 @@ def test_id_token_acr(self): _pr_resp = self.endpoint.parse_request(_req) _resp = self.endpoint.process_request(_pr_resp) res = verify_id_token( - _resp["response_args"], - keyjar=self.endpoint.server_get("endpoint_context").keyjar, + _resp["response_args"], keyjar=self.endpoint.server_get("endpoint_context").keyjar, ) assert res res = _resp["response_args"][verified_claim_name("id_token")] @@ -451,9 +437,7 @@ def test_id_token_acr(self): def test_verify_uri_unknown_client(self): request = {"redirect_uri": "https://rp.example.com/cb"} with pytest.raises(UnknownClient): - verify_uri( - self.endpoint.server_get("endpoint_context"), request, "redirect_uri" - ) + verify_uri(self.endpoint.server_get("endpoint_context"), request, "redirect_uri") def test_verify_uri_fragment(self): _ec = self.endpoint.server_get("endpoint_context") @@ -471,9 +455,7 @@ def test_verify_uri_noregistered(self): def test_verify_uri_unregistered(self): _ec = self.endpoint.server_get("endpoint_context") - _ec.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/auth_cb", {})] - } + _ec.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/auth_cb", {})]} request = {"redirect_uri": "https://rp.example.com/cb"} @@ -482,9 +464,7 @@ def test_verify_uri_unregistered(self): def test_verify_uri_qp_match(self): _ec = self.endpoint.server_get("endpoint_context") - _ec.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"]})] - } + _ec.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"]})]} request = {"redirect_uri": "https://rp.example.com/cb?foo=bar"} @@ -492,9 +472,7 @@ def test_verify_uri_qp_match(self): def test_verify_uri_qp_mismatch(self): _ec = self.endpoint.server_get("endpoint_context") - _ec.cdb["client_id"] = { - "redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"]})] - } + _ec.cdb["client_id"] = {"redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"]})]} request = {"redirect_uri": "https://rp.example.com/cb?foo=bob"} with pytest.raises(ValueError): @@ -515,9 +493,7 @@ def test_verify_uri_qp_mismatch(self): def test_verify_uri_qp_missing(self): _ec = self.endpoint.server_get("endpoint_context") _ec.cdb["client_id"] = { - "redirect_uris": [ - ("https://rp.example.com/cb", {"foo": ["bar"], "state": ["low"]}) - ] + "redirect_uris": [("https://rp.example.com/cb", {"foo": ["bar"], "state": ["low"]})] } request = {"redirect_uri": "https://rp.example.com/cb?foo=bar"} @@ -622,9 +598,9 @@ def test_setup_auth(self): "id_token_signed_response_alg": "RS256", } - kaka = self.endpoint.server_get( - "endpoint_context" - ).cookie_handler.make_cookie_content("value", "sso") + kaka = self.endpoint.server_get("endpoint_context").cookie_handler.make_cookie_content( + "value", "sso" + ) res = self.endpoint.setup_auth(request, redirect_uri, cinfo, [kaka]) assert set(res.keys()) == {"session_id", "identity", "user"} @@ -678,9 +654,7 @@ def test_setup_auth_user(self): session_id = self._create_session(request) item = _ec.authn_broker.db["anon"] - item["method"].user = b64e( - as_bytes(json.dumps({"uid": "krall", "sid": session_id})) - ) + item["method"].user = b64e(as_bytes(json.dumps({"uid": "krall", "sid": session_id}))) res = self.endpoint.setup_auth(request, redirect_uri, cinfo, None) assert set(res.keys()) == {"session_id", "identity", "user"} @@ -706,9 +680,7 @@ def test_setup_auth_session_revoked(self): session_id = self._create_session(request) item = _ec.authn_broker.db["anon"] - item["method"].user = b64e( - as_bytes(json.dumps({"uid": "krall", "sid": session_id})) - ) + item["method"].user = b64e(as_bytes(json.dumps({"uid": "krall", "sid": session_id}))) grant = _ec.session_manager[session_id] grant.revoked = True @@ -786,8 +758,7 @@ def test_post_logout_uri(self): def test_parse_request(self): _jwt = JWT(key_jar=self.rp_keyjar, iss="client_1", sign_alg="HS256") _jws = _jwt.pack( - AUTH_REQ_DICT, - aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], + AUTH_REQ_DICT, aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], ) # ----------------- _req = self.endpoint.parse_request( @@ -804,8 +775,7 @@ def test_parse_request(self): def test_parse_request_uri(self): _jwt = JWT(key_jar=self.rp_keyjar, iss="client_1", sign_alg="HS256") _jws = _jwt.pack( - AUTH_REQ_DICT, - aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], + AUTH_REQ_DICT, aud=self.endpoint.server_get("endpoint_context").provider_info["issuer"], ) request_uri = "https://client.example.com/req" @@ -868,8 +838,7 @@ def test_mint_token_exp_at(self, exp_in): def test_do_request_uri(self): request = AuthorizationRequest( - redirect_uri="https://rp.example.com/cb", - request_uri="https://example.com/request", + redirect_uri="https://rp.example.com/cb", request_uri="https://example.com/request", ) orig_request = AuthorizationRequest( @@ -887,9 +856,7 @@ def test_do_request_uri(self): ) endpoint_context = self.endpoint.server_get("endpoint_context") - endpoint_context.cdb["client_1"]["request_uris"] = [ - ("https://example.com/request", {}) - ] + endpoint_context.cdb["client_1"]["request_uris"] = [("https://example.com/request", {})] with responses.RequestsMock() as rsps: rsps.add( @@ -968,9 +935,7 @@ def test_response_mode(self, response_mode): request, response_args, request["redirect_uri"], fragment_enc=True ) else: - info = self.endpoint.response_mode( - request, response_args, request["redirect_uri"] - ) + info = self.endpoint.response_mode(request, response_args, request["redirect_uri"]) if response_mode == "form_post": assert set(info.keys()) == { @@ -1013,10 +978,7 @@ def test_do_request_user(self): endpoint_context = self.endpoint.server_get("endpoint_context") # userinfo _userinfo = init_user_info( - { - "class": "oidcop.user_info.UserInfo", - "kwargs": {"db_file": full_path("users.json")}, - }, + {"class": "oidcop.user_info.UserInfo", "kwargs": {"db_file": full_path("users.json")},}, "", ) # login_hint @@ -1052,9 +1014,7 @@ def test_authn_args_gather_message(): assert set(args.keys()) == {"query", "authn_class_ref", "return_uri", "policy_uri"} with pytest.raises(ValueError): - authn_args_gather( - request.to_urlencoded(), INTERNETPROTOCOLPASSWORD, client_info - ) + authn_args_gather(request.to_urlencoded(), INTERNETPROTOCOLPASSWORD, client_info) def test_inputs(): @@ -1068,9 +1028,10 @@ def test_inputs(): def test_acr_claims(): assert acr_claims({"claims": {"id_token": {"acr": {"value": "foo"}}}}) == ["foo"] - assert acr_claims( - {"claims": {"id_token": {"acr": {"values": ["foo", "bar"]}}}} - ) == ["foo", "bar"] + assert acr_claims({"claims": {"id_token": {"acr": {"values": ["foo", "bar"]}}}}) == [ + "foo", + "bar", + ] assert acr_claims({"claims": {"id_token": {"acr": {"values": ["foo"]}}}}) == ["foo"] assert acr_claims({"claims": {"id_token": {"acr": {"essential": True}}}}) is None @@ -1113,21 +1074,15 @@ def create_endpoint_context(self): "passwd_label": "Secret sauce", }, }, - "anon": { - "acr": UNSPECIFIED, - "class": NoAuthn, - "kwargs": {"user": "diana"}, - }, + "anon": {"acr": UNSPECIFIED, "class": NoAuthn, "kwargs": {"user": "diana"},}, }, "cookie_handler": { "class": "oidcop.cookie_handler.CookieHandler", - "kwargs": { - "sign_key": "ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch" - }, + "kwargs": {"sign_key": "ghsNKDDLshZTPn974nOsIGhedULrsqnsGoBFBLwUKuJhE2ch"}, }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) self.endpoint_context = server.endpoint_context def test_authenticated_as_without_cookie(self): @@ -1152,7 +1107,5 @@ def test_authenticated_as_with_cookie(self): client_id=authn_req["client_id"], ) - _info, _time_stamp = method.authenticated_as( - client_id="client 12345", cookie=[_cookie] - ) + _info, _time_stamp = method.authenticated_as(client_id="client 12345", cookie=[_cookie]) assert _info["sub"] == "diana" diff --git a/tests/test_26_oidc_userinfo_endpoint.py b/tests/test_26_oidc_userinfo_endpoint.py index 42245810..e607a230 100755 --- a/tests/test_26_oidc_userinfo_endpoint.py +++ b/tests/test_26_oidc_userinfo_endpoint.py @@ -1,6 +1,7 @@ import json import os +from oidcop.configure import OPConfiguration import pytest from oidcmsg.oauth2 import ResponseMessage from oidcmsg.oidc import AccessTokenRequest @@ -104,16 +105,8 @@ def create_endpoint(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "registration", - "class": Registration, - "kwargs": {}, - }, - "authorization": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "registration": {"path": "registration", "class": Registration, "kwargs": {},}, + "authorization": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token": { "path": "token", "class": Token, @@ -130,11 +123,7 @@ def create_endpoint(self): "path": "userinfo", "class": userinfo.UserInfo, "kwargs": { - "claim_types_supported": [ - "normal", - "aggregated", - "distributed", - ], + "claim_types_supported": ["normal", "aggregated", "distributed",], "client_authn_method": ["bearer_header"], }, }, @@ -169,7 +158,8 @@ def create_endpoint(self): } }, } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context endpoint_context.cdb["client_1"] = { "client_secret": "hemligt", @@ -218,9 +208,7 @@ def _mint_token(self, token_type, grant, session_id, token_ref=None): def test_init(self): assert self.endpoint assert set( - self.endpoint.server_get("endpoint_context").provider_info[ - "claims_supported" - ] + self.endpoint.server_get("endpoint_context").provider_info["claims_supported"] ) == { "address", "birthdate", @@ -251,9 +239,7 @@ def test_parse(self): # Free standing access token, not based on an authorization code access_token = self._mint_token("access_token", grant, session_id) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) assert set(_req.keys()) == {"client_id", "access_token"} assert _req["client_id"] == AUTH_REQ["client_id"] @@ -270,9 +256,7 @@ def test_process_request(self): code = self._mint_code(grant, session_id) access_token = self._mint_token("access_token", grant, session_id, code) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req, http_info=http_info) @@ -290,9 +274,7 @@ def test_process_request_not_allowed(self): _event["authn_time"] -= 9000 _event["valid_until"] -= 9000 - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req, http_info=http_info) @@ -304,9 +286,7 @@ def test_do_response(self): code = self._mint_code(grant, session_id) access_token = self._mint_token("access_token", grant, session_id, code) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req) @@ -324,9 +304,7 @@ def test_do_signed_response(self): code = self._mint_code(grant, session_id) access_token = self._mint_token("access_token", grant, session_id, code) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req) @@ -343,20 +321,14 @@ def test_custom_scope(self): access_token = self._mint_token("access_token", grant, session_id) self.endpoint.kwargs["add_claims_by_scope"] = True - self.endpoint.server_get( - "endpoint_context" - ).claims_interface.add_claims_by_scope = True + self.endpoint.server_get("endpoint_context").claims_interface.add_claims_by_scope = True grant.claims = { - "userinfo": self.endpoint.server_get( - "endpoint_context" - ).claims_interface.get_claims( + "userinfo": self.endpoint.server_get("endpoint_context").claims_interface.get_claims( session_id=session_id, scopes=_auth_req["scope"], usage="userinfo" ) } - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req, http_info=http_info) @@ -378,9 +350,7 @@ def test_wrong_type_of_token(self): grant = self.session_manager[session_id] refresh_token = self._mint_token("refresh_token", grant, session_id) - http_info = { - "headers": {"authorization": "Bearer {}".format(refresh_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(refresh_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) args = self.endpoint.process_request(_req, http_info=http_info) @@ -395,9 +365,7 @@ def test_invalid_token(self): grant = self.session_manager[session_id] access_token = self._mint_token("access_token", grant, session_id) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint.parse_request({}, http_info=http_info) access_token.expires_at = time_sans_frac() - 10 diff --git a/tests/test_30_oidc_end_session.py b/tests/test_30_oidc_end_session.py index 6634b53f..1d827a48 100644 --- a/tests/test_30_oidc_end_session.py +++ b/tests/test_30_oidc_end_session.py @@ -4,15 +4,16 @@ from urllib.parse import parse_qs from urllib.parse import urlparse -import pytest -import responses from cryptojwt.key_jar import build_keyjar from oidcmsg.exception import InvalidRequest from oidcmsg.message import Message from oidcmsg.oidc import AuthorizationRequest from oidcmsg.oidc import verified_claim_name from oidcmsg.oidc import verify_id_token +import pytest +import responses +from oidcop.configure import OPConfiguration from oidcop.cookie_handler import CookieHandler from oidcop.exception import RedirectURIError from oidcop.oauth2.authorization import join_query @@ -149,9 +150,7 @@ def create_endpoint(self): "jwks_def": { "private_path": "private/token_jwks.json", "read_only": False, - "key_defs": [ - {"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"} - ], + "key_defs": [{"type": "oct", "bytes": "24", "use": ["enc"], "kid": "code"}], }, "code": {"kwargs": {"lifetime": 600}}, "token": { @@ -187,7 +186,12 @@ def create_endpoint(self): }, } self.cd = CookieHandler(**cookie_conf) - server = Server(conf, cookie_handler=self.cd, keyjar=KEYJAR) + server = Server( + OPConfiguration(conf=conf, base_path=BASEDIR), + cwd=BASEDIR, + cookie_handler=self.cd, + keyjar=KEYJAR, + ) endpoint_context = server.endpoint_context endpoint_context.cdb = { "client_1": { @@ -258,9 +262,9 @@ def _auth_with_id_token(self, state): _pr_resp = self.authn_endpoint.parse_request(req.to_dict()) _resp = self.authn_endpoint.process_request(_pr_resp) - _info = self.session_endpoint.server_get( - "endpoint_context" - ).cookie_handler.parse_cookie("oidc_op", _resp["cookie"]) + _info = self.session_endpoint.server_get("endpoint_context").cookie_handler.parse_cookie( + "oidc_op", _resp["cookie"] + ) # value is a JSON document _cookie_info = json.loads(_info[0]["value"]) @@ -272,9 +276,7 @@ def test_end_session_endpoint_with_cookie(self): _session_info = self.session_manager.get_session_info_by_token(_code) cookie = self._create_cookie(_session_info["session_id"]) http_info = {"cookie": [cookie]} - _req_args = self.session_endpoint.parse_request( - {"state": "1234567"}, http_info=http_info - ) + _req_args = self.session_endpoint.parse_request({"state": "1234567"}, http_info=http_info) resp = self.session_endpoint.process_request(_req_args, http_info=http_info) # returns a signed JWT to be put in a verification web page shown to @@ -293,9 +295,7 @@ def test_end_session_endpoint_with_cookie_and_unknown_sid(self): id_token = resp_args["id_token"] _uid, _cid, _gid = self.session_manager.decrypt_session_id(_session_id) - cookie = self._create_cookie( - self.session_manager.session_key(_uid, "client_66", _gid) - ) + cookie = self._create_cookie(self.session_manager.session_key(_uid, "client_66", _gid)) http_info = {"cookie": [cookie]} with pytest.raises(ValueError): @@ -307,20 +307,14 @@ def test_end_session_endpoint_with_cookie_id_token_and_unknown_sid(self): id_token = resp_args["id_token"] _uid, _cid, _gid = self.session_manager.decrypt_session_id(_session_id) - cookie = self._create_cookie( - self.session_manager.session_key(_uid, "client_66", _gid) - ) + cookie = self._create_cookie(self.session_manager.session_key(_uid, "client_66", _gid)) http_info = {"cookie": [cookie]} msg = Message(id_token=id_token) - verify_id_token( - msg, keyjar=self.session_endpoint.server_get("endpoint_context").keyjar - ) + verify_id_token(msg, keyjar=self.session_endpoint.server_get("endpoint_context").keyjar) msg2 = Message(id_token_hint=id_token) - msg2[verified_claim_name("id_token_hint")] = msg[ - verified_claim_name("id_token") - ] + msg2[verified_claim_name("id_token_hint")] = msg[verified_claim_name("id_token")] with pytest.raises(ValueError): self.session_endpoint.process_request(msg2, http_info=http_info) @@ -332,9 +326,7 @@ def test_end_session_endpoint_with_cookie_dual_login(self): cookie = self._create_cookie(_session_info["session_id"]) http_info = {"cookie": [cookie]} - resp = self.session_endpoint.process_request( - {"state": "abcde"}, http_info=http_info - ) + resp = self.session_endpoint.process_request({"state": "abcde"}, http_info=http_info) # returns a signed JWT to be put in a verification web page shown to # the user @@ -362,10 +354,7 @@ def test_end_session_endpoint_with_post_logout_redirect_uri(self): with pytest.raises(InvalidRequest): self.session_endpoint.process_request( - { - "post_logout_redirect_uri": post_logout_redirect_uri, - "state": "abcde", - }, + {"post_logout_redirect_uri": post_logout_redirect_uri, "state": "abcde",}, http_info=http_info, ) @@ -382,9 +371,7 @@ def test_end_session_endpoint_with_wrong_post_logout_redirect_uri(self): post_logout_redirect_uri = "https://demo.example.com/log_out" msg = Message(id_token=id_token) - verify_id_token( - msg, keyjar=self.session_endpoint.server_get("endpoint_context").keyjar - ) + verify_id_token(msg, keyjar=self.session_endpoint.server_get("endpoint_context").keyjar) with pytest.raises(RedirectURIError): self.session_endpoint.process_request( @@ -392,9 +379,7 @@ def test_end_session_endpoint_with_wrong_post_logout_redirect_uri(self): "post_logout_redirect_uri": post_logout_redirect_uri, "state": "abcde", "id_token_hint": id_token, - verified_claim_name("id_token_hint"): msg[ - verified_claim_name("id_token") - ], + verified_claim_name("id_token_hint"): msg[verified_claim_name("id_token")], }, http_info=http_info, ) @@ -410,9 +395,7 @@ def test_back_channel_logout_no_uri(self): def test_back_channel_logout(self): self._code_auth("1234567") - _cdb = copy.copy( - self.session_endpoint.server_get("endpoint_context").cdb["client_1"] - ) + _cdb = copy.copy(self.session_endpoint.server_get("endpoint_context").cdb["client_1"]) _cdb["backchannel_logout_uri"] = "https://example.com/bc_logout" _cdb["client_id"] = "client_1" res = self.session_endpoint.do_back_channel_logout(_cdb, "_sid_") @@ -427,9 +410,7 @@ def test_back_channel_logout(self): def test_front_channel_logout(self): self._code_auth("1234567") - _cdb = copy.copy( - self.session_endpoint.server_get("endpoint_context").cdb["client_1"] - ) + _cdb = copy.copy(self.session_endpoint.server_get("endpoint_context").cdb["client_1"]) _cdb["frontchannel_logout_uri"] = "https://example.com/fc_logout" _cdb["client_id"] = "client_1" res = do_front_channel_logout_iframe(_cdb, ISS, "_sid_") @@ -438,9 +419,7 @@ def test_front_channel_logout(self): def test_front_channel_logout_session_required(self): self._code_auth("1234567") - _cdb = copy.copy( - self.session_endpoint.server_get("endpoint_context").cdb["client_1"] - ) + _cdb = copy.copy(self.session_endpoint.server_get("endpoint_context").cdb["client_1"]) _cdb["frontchannel_logout_uri"] = "https://example.com/fc_logout" _cdb["frontchannel_logout_session_required"] = True _cdb["client_id"] = "client_1" @@ -456,9 +435,7 @@ def test_front_channel_logout_session_required(self): def test_front_channel_logout_with_query(self): self._code_auth("1234567") - _cdb = copy.copy( - self.session_endpoint.server_get("endpoint_context").cdb["client_1"] - ) + _cdb = copy.copy(self.session_endpoint.server_get("endpoint_context").cdb["client_1"]) _cdb["frontchannel_logout_uri"] = "https://example.com/fc_logout?entity_id=foo" _cdb["frontchannel_logout_session_required"] = True _cdb["client_id"] = "client_1" @@ -496,9 +473,7 @@ def test_logout_from_client_bc(self): assert _jwt assert _jwt["iss"] == ISS assert _jwt["aud"] == ["client_1"] - assert ( - "sid" in _jwt - ) # This session ID is not the same as the session_id mentioned above + assert "sid" in _jwt # This session ID is not the same as the session_id mentioned above _sid = self.session_endpoint._decrypt_sid(_jwt["sid"]) assert _sid == _session_info["session_id"] @@ -592,9 +567,7 @@ def test_logout_from_client_unknow_sid(self): _session_info = self.session_manager.get_session_info_by_token(_code) self._code_auth2("abcdefg") - _uid, _cid, _gid = self.session_manager.decrypt_session_id( - _session_info["session_id"] - ) + _uid, _cid, _gid = self.session_manager.decrypt_session_id(_session_info["session_id"]) _sid = self.session_manager.encrypted_session_id("babs", _cid, _gid) with pytest.raises(KeyError): res = self.session_endpoint.logout_all_clients(_sid) @@ -619,12 +592,8 @@ def test_logout_from_client_no_session(self): "client_id" ] = "client_2" - _uid, _cid, _gid = self.session_manager.decrypt_session_id( - _session_info["session_id"] - ) - self.session_endpoint.server_get("endpoint_context").session_manager.delete( - [_uid, _cid] - ) + _uid, _cid, _gid = self.session_manager.decrypt_session_id(_session_info["session_id"]) + self.session_endpoint.server_get("endpoint_context").session_manager.delete([_uid, _cid]) with pytest.raises(ValueError): self.session_endpoint.logout_all_clients(_session_info["session_id"]) diff --git a/tests/test_31_introspection.py b/tests/test_31_oauth2_introspection.py similarity index 92% rename from tests/test_31_introspection.py rename to tests/test_31_oauth2_introspection.py index a948e8a9..ae497157 100644 --- a/tests/test_31_introspection.py +++ b/tests/test_31_oauth2_introspection.py @@ -2,6 +2,9 @@ import json import os +from oidcop.configure import ASConfiguration + +from oidcop.configure import OPConfiguration import pytest from cryptojwt import JWT from cryptojwt import as_unicode @@ -118,15 +121,6 @@ def create_endpoint(self, jwt_token): "class": "oidcop.token.jwt_token.JWTToken", "kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"],}, }, - "id_token": { - "class": "oidcop.token.id_token.IDToken", - "kwargs": { - "base_claims": { - "email": {"essential": True}, - "email_verified": {"essential": True}, - } - }, - }, }, "endpoint": { "authorization": { @@ -175,11 +169,7 @@ def create_endpoint(self, jwt_token): "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": {}, @@ -197,7 +187,7 @@ def create_endpoint(self, jwt_token): "class": "oidcop.token.jwt_token.JWTToken", "kwargs": {}, } - server = Server(conf) + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) endpoint_context = server.endpoint_context endpoint_context.cdb["client_1"] = { "client_secret": "hemligt", @@ -205,14 +195,10 @@ def create_endpoint(self, jwt_token): "client_salt": "salted", "token_endpoint_auth_method": "client_secret_post", "response_types": ["code", "token", "code id_token", "id_token"], - "introspection_claims": { - "nickname": None, - "eduperson_scoped_affiliation": None, - }, + "introspection_claims": {"nickname": None, "eduperson_scoped_affiliation": None,}, } endpoint_context.keyjar.import_jwks_as_json( - endpoint_context.keyjar.export_jwks_as_json(private=True), - endpoint_context.issuer, + endpoint_context.keyjar.export_jwks_as_json(private=True), endpoint_context.issuer, ) self.introspection_endpoint = server.server_get("endpoint", "introspection") self.token_endpoint = server.server_get("endpoint", "token") @@ -246,9 +232,7 @@ def _mint_token(self, type, grant, session_id, based_on=None, **kwargs): def _get_access_token(self, areq): session_id = self._create_session(areq) # Consent handling - grant = self.token_endpoint.server_get("endpoint_context").authz( - session_id, areq - ) + grant = self.token_endpoint.server_get("endpoint_context").authz(session_id, areq) self.session_manager[session_id] = grant # grant = self.session_manager[session_id] code = self._mint_token("authorization_code", grant, session_id) @@ -299,9 +283,9 @@ def test_process_request(self): { "token": access_token.value, "client_id": "client_1", - "client_secret": self.introspection_endpoint.server_get( - "endpoint_context" - ).cdb["client_1"]["client_secret"], + "client_secret": self.introspection_endpoint.server_get("endpoint_context").cdb[ + "client_1" + ]["client_secret"], } ) _resp = self.introspection_endpoint.process_request(_req) @@ -323,9 +307,9 @@ def test_do_response(self): { "token": access_token.value, "client_id": "client_1", - "client_secret": self.introspection_endpoint.server_get( - "endpoint_context" - ).cdb["client_1"]["client_secret"], + "client_secret": self.introspection_endpoint.server_get("endpoint_context").cdb[ + "client_1" + ]["client_secret"], } ) _resp = self.introspection_endpoint.process_request(_req) @@ -355,10 +339,7 @@ def test_do_response_no_token(self): # access_token = self._get_access_token(AUTH_REQ) _context = self.introspection_endpoint.server_get("endpoint_context") _req = self.introspection_endpoint.parse_request( - { - "client_id": "client_1", - "client_secret": _context.cdb["client_1"]["client_secret"], - } + {"client_id": "client_1", "client_secret": _context.cdb["client_1"]["client_secret"],} ) _resp = self.introspection_endpoint.process_request(_req) assert "error" in _resp @@ -383,9 +364,7 @@ def test_code(self): session_id = self._create_session(AUTH_REQ) # Apply consent - grant = self.token_endpoint.server_get("endpoint_context").authz( - session_id, AUTH_REQ - ) + grant = self.token_endpoint.server_get("endpoint_context").authz(session_id, AUTH_REQ) self.session_manager[session_id] = grant code = self._mint_token("authorization_code", grant, session_id) @@ -406,9 +385,7 @@ def test_code(self): def test_introspection_claims(self): session_id = self._create_session(AUTH_REQ) # Apply consent - grant = self.token_endpoint.server_get("endpoint_context").authz( - session_id, AUTH_REQ - ) + grant = self.token_endpoint.server_get("endpoint_context").authz(session_id, AUTH_REQ) self.session_manager[session_id] = grant code = self._mint_token("authorization_code", grant, session_id) @@ -416,9 +393,7 @@ def test_introspection_claims(self): self.introspection_endpoint.kwargs["enable_claims_per_client"] = True - _c_interface = self.introspection_endpoint.server_get( - "endpoint_context" - ).claims_interface + _c_interface = self.introspection_endpoint.server_get("endpoint_context").claims_interface grant.claims = { "introspection": _c_interface.get_claims( session_id, scopes=AUTH_REQ["scope"], usage="introspection" diff --git a/tests/test_32_read_registration.py b/tests/test_32_oidc_read_registration.py similarity index 91% rename from tests/test_32_read_registration.py rename to tests/test_32_oidc_read_registration.py index fcb37dcd..c687a0ec 100644 --- a/tests/test_32_read_registration.py +++ b/tests/test_32_oidc_read_registration.py @@ -1,6 +1,8 @@ # -*- coding: latin-1 -*- import json +import os +from oidcop.configure import OPConfiguration import pytest from oidcmsg.oidc import RegistrationRequest @@ -12,6 +14,8 @@ from oidcop.oidc.userinfo import UserInfo from oidcop.server import Server +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + KEYDEFS = [ {"type": "RSA", "key": "", "use": ["sig"]}, {"type": "EC", "crv": "P-256", "use": ["sig"]}, @@ -97,11 +101,7 @@ def create_endpoint(self): "class": RegistrationRead, "kwargs": {"client_authn_method": ["bearer_header"]}, }, - "authorization": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "authorization": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token": { "path": "token", "class": Token, @@ -118,11 +118,9 @@ def create_endpoint(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) self.registration_endpoint = server.server_get("endpoint", "registration") - self.registration_api_endpoint = server.server_get( - "endpoint", "registration_read" - ) + self.registration_api_endpoint = server.server_get("endpoint", "registration_read") def test_do_response(self): _req = self.registration_endpoint.parse_request(CLI_REQ.to_json()) @@ -141,8 +139,7 @@ def test_do_response(self): } _api_req = self.registration_api_endpoint.parse_request( - "client_id={}".format(_resp["response_args"]["client_id"]), - http_info=http_info, + "client_id={}".format(_resp["response_args"]["client_id"]), http_info=http_info, ) assert set(_api_req.keys()) == {"client_id"} @@ -152,6 +149,4 @@ def test_do_response(self): _endp_response = self.registration_api_endpoint.do_response(_info) assert set(_endp_response.keys()) == {"response", "http_headers"} - assert ("Content-type", "application/json; charset=utf-8") in _endp_response[ - "http_headers" - ] + assert ("Content-type", "application/json; charset=utf-8") in _endp_response["http_headers"] diff --git a/tests/test_33_pkce.py b/tests/test_33_oauth2_pkce.py similarity index 95% rename from tests/test_33_pkce.py rename to tests/test_33_oauth2_pkce.py index b920fa7e..08c24d7d 100644 --- a/tests/test_33_pkce.py +++ b/tests/test_33_oauth2_pkce.py @@ -4,6 +4,7 @@ import secrets import string +from oidcop.configure import ASConfiguration import pytest import yaml from oidcmsg.message import Message @@ -121,11 +122,7 @@ def conf(): "capabilities": CAPABILITIES, "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, "endpoint": { - "authorization": { - "path": "{}/authorization", - "class": Authorization, - "kwargs": {}, - }, + "authorization": {"path": "{}/authorization", "class": Authorization, "kwargs": {},}, "token": { "path": "{}/token", "class": Token, @@ -193,7 +190,8 @@ def _code_challenge(): def create_server(config): - server = Server(config) + server = Server(ASConfiguration(conf=config, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) endpoint_context.cdb = _clients["oidc_clients"] @@ -296,9 +294,7 @@ def test_unknown_code_challenge_method(self): assert isinstance(_pr_resp, AuthorizationErrorResponse) assert _pr_resp["error"] == "invalid_request" - assert _pr_resp[ - "error_description" - ] == "Unsupported code_challenge_method={}".format( + assert _pr_resp["error_description"] == "Unsupported code_challenge_method={}".format( _authn_req["code_challenge_method"] ) @@ -316,9 +312,7 @@ def test_unsupported_code_challenge_method(self, conf): assert isinstance(_pr_resp, AuthorizationErrorResponse) assert _pr_resp["error"] == "invalid_request" - assert _pr_resp[ - "error_description" - ] == "Unsupported code_challenge_method={}".format( + assert _pr_resp["error_description"] == "Unsupported code_challenge_method={}".format( _authn_req["code_challenge_method"] ) @@ -371,9 +365,7 @@ def test_missing_authz_endpoint(): } }, } - configuration = OPConfiguration( - conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf, base_path=BASEDIR, domain="127.0.0.1", port=443) server = Server(configuration) add_pkce_support(server.server_get("endpoints")) @@ -398,9 +390,7 @@ def test_missing_token_endpoint(): }, }, } - configuration = OPConfiguration( - conf, base_path=BASEDIR, domain="127.0.0.1", port=443 - ) + configuration = OPConfiguration(conf, base_path=BASEDIR, domain="127.0.0.1", port=443) server = Server(configuration) add_pkce_support(server.server_get("endpoints")) diff --git a/tests/test_34_sso.py b/tests/test_34_oidc_sso.py similarity index 94% rename from tests/test_34_sso.py rename to tests/test_34_oidc_sso.py index 075bd58a..dca54792 100755 --- a/tests/test_34_sso.py +++ b/tests/test_34_oidc_sso.py @@ -2,6 +2,7 @@ import json import os +from oidcop.configure import OPConfiguration import pytest import yaml from cryptojwt import KeyJar @@ -133,9 +134,7 @@ def create_endpoint_context(self): "path": "{}/authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -145,11 +144,7 @@ def create_endpoint_context(self): }, "keys": {"uri_path": "static/jwks.json", "key_defs": KEYDEFS}, "authentication": { - "anon": { - "acr": UNSPECIFIED, - "class": NoAuthn, - "kwargs": {"user": "diana"}, - }, + "anon": {"acr": UNSPECIFIED, "class": NoAuthn, "kwargs": {"user": "diana"},}, }, "cookie_handler": { "class": "oidcop.cookie_handler.CookieHandler", @@ -164,7 +159,8 @@ def create_endpoint_context(self): }, "template_dir": "template", } - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) endpoint_context.cdb = _clients["oidc_clients"] @@ -234,9 +230,9 @@ def test_sso(self): # No valid login cookie so new session assert info["session_id"] != sid2 - user_session_info = self.endpoint.server_get( - "endpoint_context" - ).session_manager.get(["diana"]) + user_session_info = self.endpoint.server_get("endpoint_context").session_manager.get( + ["diana"] + ) assert len(user_session_info.subordinate) == 3 assert set(user_session_info.subordinate) == { "client_1", diff --git a/tests/test_35_oidc_token_endpoint.py b/tests/test_35_oidc_token_endpoint.py index 4e3c1694..ec0bd6a4 100755 --- a/tests/test_35_oidc_token_endpoint.py +++ b/tests/test_35_oidc_token_endpoint.py @@ -1,6 +1,7 @@ import json import os +from oidcop.configure import OPConfiguration import pytest from cryptojwt import JWT from cryptojwt.key_jar import build_keyjar @@ -126,16 +127,8 @@ def conf(): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "registration", - "class": Registration, - "kwargs": {}, - }, - "authorization": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "registration": {"path": "registration", "class": Registration, "kwargs": {},}, + "authorization": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token": { "path": "token", "class": Token, @@ -171,11 +164,7 @@ def conf(): "usage_rules": { "authorization_code": { "expires_in": 300, - "supports_minting": [ - "access_token", - "refresh_token", - "id_token", - ], + "supports_minting": ["access_token", "refresh_token", "id_token",], "max_usage": 1, }, "access_token": {"expires_in": 600}, @@ -194,7 +183,8 @@ def conf(): class TestEndpoint(object): @pytest.fixture(autouse=True) def create_endpoint(self, conf): - server = Server(conf) + server = Server(OPConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) + endpoint_context = server.endpoint_context endpoint_context.cdb["client_1"] = { "client_secret": "hemligt", @@ -225,9 +215,7 @@ def _create_session(self, auth_req, sub_type="public", sector_identifier=""): ) def _mint_code(self, grant, client_id): - session_id = self.session_manager.encrypted_session_id( - self.user_id, client_id, grant.id - ) + session_id = self.session_manager.encrypted_session_id(self.user_id, client_id, grant.id) usage_rules = grant.usage_rules.get("authorization_code", {}) _exp_in = usage_rules.get("expires_in") @@ -334,9 +322,7 @@ def test_process_request_using_private_key_jwt(self): _jwt = JWT(CLIENT_KEYJAR, iss=AUTH_REQ["client_id"], sign_alg="RS256") _jwt.with_jti = True _assertion = _jwt.pack({"aud": [self.token_endpoint.full_path]}) - _token_request.update( - {"client_assertion": _assertion, "client_assertion_type": JWT_BEARER} - ) + _token_request.update({"client_assertion": _assertion, "client_assertion_type": JWT_BEARER}) _token_request["code"] = code.value _req = self.token_endpoint.parse_request(_token_request) @@ -366,9 +352,7 @@ def test_do_refresh_access_token(self): _token_value = _resp["response_args"]["refresh_token"] _session_info = self.session_manager.get_session_info_by_token(_token_value) - _token = self.session_manager.find_token( - _session_info["session_id"], _token_value - ) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) _token.usage_rules["supports_minting"] = [ "access_token", "refresh_token", @@ -410,9 +394,7 @@ def test_do_2nd_refresh_access_token(self): # Make sure ID Tokens can also be used by this refesh token _token_value = _resp["response_args"]["refresh_token"] _session_info = self.session_manager.get_session_info_by_token(_token_value) - _token = self.session_manager.find_token( - _session_info["session_id"], _token_value - ) + _token = self.session_manager.find_token(_session_info["session_id"], _token_value) _token.usage_rules["supports_minting"] = [ "access_token", "refresh_token", diff --git a/tests/test_36_token_exchange.py b/tests/test_36_oauth2_token_exchange.py similarity index 91% rename from tests/test_36_token_exchange.py rename to tests/test_36_oauth2_token_exchange.py index 7e6904e9..5e7641c1 100644 --- a/tests/test_36_token_exchange.py +++ b/tests/test_36_oauth2_token_exchange.py @@ -1,18 +1,19 @@ import json import os -import pytest from cryptojwt.key_jar import build_keyjar from oidcmsg.oauth2 import TokenExchangeRequest from oidcmsg.oidc import AccessTokenRequest from oidcmsg.oidc import AuthorizationRequest +import pytest from oidcop.authn_event import create_authn_event from oidcop.authz import AuthzHandling from oidcop.client_authn import verify_client +from oidcop.configure import ASConfiguration from oidcop.cookie_handler import CookieHandler -from oidcop.oidc.authorization import Authorization -from oidcop.oidc.token import Token +from oidcop.oauth2.authorization import Authorization +from oidcop.oauth2.token import Token from oidcop.server import Server from oidcop.session.grant import ExchangeGrant from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD @@ -95,12 +96,12 @@ def create_endpoint(self): "endpoint": { "authorization": { "path": "authorization", - "class": Authorization, + "class": 'oidcop.oauth2.authorization.Authorization', "kwargs": {}, }, "token": { "path": "token", - "class": Token, + "class": 'oidcop.oauth2.token.Token', "kwargs": { "client_authn_method": [ "client_secret_basic", @@ -132,11 +133,7 @@ def create_endpoint(self): "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": {}, @@ -161,12 +158,11 @@ def create_endpoint(self): }, "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", "kwargs": {}}, }, } - server = Server(conf) + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) endpoint_context = server.endpoint_context endpoint_context.cdb["client_1"] = { "client_secret": "hemligt", @@ -261,22 +257,15 @@ def test_do_response(self): assert exch_grants exch_grant = exch_grants[0] - session_info = self.session_manager.get_session_info_by_token( - ter["subject_token"] - ) - _token = self.session_manager.find_token( - session_info["session_id"], ter["subject_token"] - ) + session_info = self.session_manager.get_session_info_by_token(ter["subject_token"]) + _token = self.session_manager.find_token(session_info["session_id"], ter["subject_token"]) session_id = self.session_manager.encrypted_session_id( session_info["user_id"], session_info["client_id"], exch_grant.id ) _token = self._mint_access_token( - exch_grant, - session_id, - token_ref=_token, - resources=["https://backend.example.com"], + exch_grant, session_id, token_ref=_token, resources=["https://backend.example.com"], ) print(_token.value) @@ -284,9 +273,9 @@ def test_do_response(self): { "token": _token.value, "client_id": "client_1", - "client_secret": self.introspection_endpoint.server_get( - "endpoint_context" - ).cdb["client_1"]["client_secret"], + "client_secret": self.introspection_endpoint.server_get("endpoint_context").cdb[ + "client_1" + ]["client_secret"], } ) _resp = self.introspection_endpoint.process_request(_req) diff --git a/tests/test_40_oauth2_pushed_authorization.py b/tests/test_40_oauth2_pushed_authorization.py index 2e4a17bb..56ef1d2d 100644 --- a/tests/test_40_oauth2_pushed_authorization.py +++ b/tests/test_40_oauth2_pushed_authorization.py @@ -1,5 +1,7 @@ import io +import os +from oidcop.configure import ASConfiguration import pytest import yaml from cryptojwt import JWT @@ -15,6 +17,8 @@ from oidcop.oidc.registration import Registration from oidcop.server import Server +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + CAPABILITIES = { "subject_types_supported": ["public", "pairwise", "ephemeral"], "grant_types_supported": [ @@ -106,18 +110,12 @@ def create_endpoint(self): "class": ProviderConfiguration, "kwargs": {}, }, - "registration": { - "path": "registration", - "class": Registration, - "kwargs": {}, - }, + "registration": {"path": "registration", "class": Registration, "kwargs": {},}, "authorization": { "path": "authorization", "class": Authorization, "kwargs": { - "response_types_supported": [ - " ".join(x) for x in RESPONSE_TYPES_SUPPORTED - ], + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], "response_modes_supported": ["query", "fragment", "form_post"], "claims_parameter_supported": True, "request_parameter_supported": True, @@ -157,7 +155,7 @@ def create_endpoint(self): }, }, } - server = Server(conf) + server = Server(ASConfiguration(conf=conf, base_path=BASEDIR), cwd=BASEDIR) endpoint_context = server.endpoint_context _clients = yaml.safe_load(io.StringIO(client_yaml)) endpoint_context.cdb = _clients["oidc_clients"] @@ -171,9 +169,7 @@ def create_endpoint(self): self.rp_keyjar.export_jwks(issuer_id="s6BhdRkqt3"), "s6BhdRkqt3" ) - self.pushed_authorization_endpoint = server.server_get( - "endpoint", "pushed_authorization" - ) + self.pushed_authorization_endpoint = server.server_get("endpoint", "pushed_authorization") self.authorization_endpoint = server.server_get("endpoint", "authorization") def test_init(self): @@ -181,14 +177,10 @@ def test_init(self): def test_pushed_auth_urlencoded(self): http_info = { - "headers": { - "authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3" - } + "headers": {"authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3"} } - _req = self.pushed_authorization_endpoint.parse_request( - AUTHN_REQUEST, http_info=http_info - ) + _req = self.pushed_authorization_endpoint.parse_request(AUTHN_REQUEST, http_info=http_info) assert isinstance(_req, AuthorizationRequest) assert set(_req.keys()) == { @@ -208,14 +200,10 @@ def test_pushed_auth_request(self): authn_request = "request={}".format(_jws) http_info = { - "headers": { - "authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3" - } + "headers": {"authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3"} } - _req = self.pushed_authorization_endpoint.parse_request( - authn_request, http_info=http_info - ) + _req = self.pushed_authorization_endpoint.parse_request(authn_request, http_info=http_info) assert isinstance(_req, AuthorizationRequest) _req = remove_jwt_parameters(_req) @@ -233,14 +221,10 @@ def test_pushed_auth_request(self): def test_pushed_auth_urlencoded_process(self): http_info = { - "headers": { - "authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3" - } + "headers": {"authorization": "Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3"} } - _req = self.pushed_authorization_endpoint.parse_request( - AUTHN_REQUEST, http_info=http_info - ) + _req = self.pushed_authorization_endpoint.parse_request(AUTHN_REQUEST, http_info=http_info) assert isinstance(_req, AuthorizationRequest) assert set(_req.keys()) == { diff --git a/tests/test_50_persistence.py b/tests/test_50_persistence.py index 419258da..11b801c8 100644 --- a/tests/test_50_persistence.py +++ b/tests/test_50_persistence.py @@ -2,14 +2,15 @@ import os import shutil -import pytest from cryptojwt.jwt import utc_time_sans_frac from oidcmsg.oidc import AccessTokenRequest from oidcmsg.oidc import AuthorizationRequest +import pytest from oidcop import user_info from oidcop.authn_event import create_authn_event from oidcop.authz import AuthzHandling +from oidcop.configure import OPConfiguration from oidcop.oidc import userinfo from oidcop.oidc.authorization import Authorization from oidcop.oidc.provider_config import ProviderConfiguration @@ -102,11 +103,7 @@ def full_path(local_file): "kwargs": {}, }, "registration": {"path": "registration", "class": Registration, "kwargs": {},}, - "authorization": { - "path": "authorization", - "class": Authorization, - "kwargs": {}, - }, + "authorization": {"path": "authorization", "class": Authorization, "kwargs": {},}, "token": { "path": "token", "class": Token, @@ -129,10 +126,7 @@ def full_path(local_file): }, }, }, - "userinfo": { - "class": user_info.UserInfo, - "kwargs": {"db_file": full_path("users.json")}, - }, + "userinfo": {"class": user_info.UserInfo, "kwargs": {"db_file": full_path("users.json")},}, # "client_authn": verify_client, "authentication": { "anon": { @@ -164,17 +158,11 @@ def full_path(local_file): "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"], - }, + "refresh_token": {"supports_minting": ["access_token", "refresh_token"],}, }, "expires_in": 43200, } @@ -191,8 +179,12 @@ def create_endpoint(self): except FileNotFoundError: pass - server1 = Server(ENDPOINT_CONTEXT_CONFIG) - server2 = Server(ENDPOINT_CONTEXT_CONFIG) + server1 = Server( + OPConfiguration(conf=ENDPOINT_CONTEXT_CONFIG, base_path=BASEDIR), cwd=BASEDIR + ) + server2 = Server( + OPConfiguration(conf=ENDPOINT_CONTEXT_CONFIG, base_path=BASEDIR), cwd=BASEDIR + ) server1.endpoint_context.cdb["client_1"] = { "client_secret": "hemligt", @@ -222,9 +214,7 @@ def create_endpoint(self): } self.user_id = "diana" - def _create_session( - self, auth_req, sub_type="public", sector_identifier="", index=1 - ): + def _create_session(self, auth_req, sub_type="public", sector_identifier="", index=1): if sector_identifier: authz_req = auth_req.copy() authz_req["sector_identifier_uri"] = sector_identifier @@ -264,9 +254,7 @@ def _mint_access_token(self, grant, session_id, token_ref=None, index=1): based_on=token_ref, # Means the token (tok) was used to mint this token ) - self.session_manager[index].set( - [self.user_id, _session_info["client_id"], grant.id], grant - ) + self.session_manager[index].set([self.user_id, _session_info["client_id"], grant.id], grant) return _token @@ -279,9 +267,7 @@ def _dump_restore(self, fro, to): def test_init(self): assert self.endpoint[1] assert set( - self.endpoint[1] - .server_get("endpoint_context") - .provider_info["claims_supported"] + self.endpoint[1].server_get("endpoint_context").provider_info["claims_supported"] ) == { "address", "birthdate", @@ -306,20 +292,12 @@ def test_init(self): "zoneinfo", } assert set( - self.endpoint[1] - .server_get("endpoint_context") - .provider_info["claims_supported"] - ) == set( - self.endpoint[2] - .server_get("endpoint_context") - .provider_info["claims_supported"] - ) + self.endpoint[1].server_get("endpoint_context").provider_info["claims_supported"] + ) == set(self.endpoint[2].server_get("endpoint_context").provider_info["claims_supported"]) def test_parse(self): session_id = self._create_session(AUTH_REQ, index=1) - grant = ( - self.endpoint[1].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[1].server_get("endpoint_context").authz(session_id, AUTH_REQ) # grant, session_id = self._do_grant(AUTH_REQ, index=1) code = self._mint_code(grant, session_id, index=1) access_token = self._mint_access_token(grant, session_id, code, 1) @@ -328,48 +306,36 @@ def test_parse(self): self._dump_restore(1, 2) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[2].parse_request({}, http_info=http_info) assert set(_req.keys()) == {"client_id", "access_token"} def test_process_request(self): session_id = self._create_session(AUTH_REQ, index=1) - grant = ( - self.endpoint[1].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[1].server_get("endpoint_context").authz(session_id, AUTH_REQ) code = self._mint_code(grant, session_id, index=1) access_token = self._mint_access_token(grant, session_id, code, 1) self._dump_restore(1, 2) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[2].parse_request({}, http_info=http_info) args = self.endpoint[2].process_request(_req) assert args def test_process_request_not_allowed(self): session_id = self._create_session(AUTH_REQ, index=2) - grant = ( - self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) code = self._mint_code(grant, session_id, index=2) access_token = self._mint_access_token(grant, session_id, code, 2) access_token.expires_at = utc_time_sans_frac() - 60 - self.session_manager[2].set( - [self.user_id, AUTH_REQ["client_id"], grant.id], grant - ) + self.session_manager[2].set([self.user_id, AUTH_REQ["client_id"], grant.id], grant) self._dump_restore(2, 1) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[1].parse_request({}, http_info=http_info) @@ -394,17 +360,13 @@ def test_process_request_not_allowed(self): def test_do_response(self): session_id = self._create_session(AUTH_REQ, index=2) - grant = ( - self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) code = self._mint_code(grant, session_id, index=2) access_token = self._mint_access_token(grant, session_id, code, 2) self._dump_restore(2, 1) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[1].parse_request({}, http_info=http_info) args = self.endpoint[1].process_request(_req) @@ -421,17 +383,13 @@ def test_do_signed_response(self): ] = "ES256" session_id = self._create_session(AUTH_REQ, index=2) - grant = ( - self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) code = self._mint_code(grant, session_id, index=2) access_token = self._mint_access_token(grant, session_id, code, 2) self._dump_restore(2, 1) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[1].parse_request({}, http_info=http_info) args = self.endpoint[1].process_request(_req) @@ -444,34 +402,26 @@ def test_custom_scope(self): _auth_req["scope"] = ["openid", "research_and_scholarship"] session_id = self._create_session(AUTH_REQ, index=2) - grant = ( - self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) - ) + grant = self.endpoint[2].server_get("endpoint_context").authz(session_id, AUTH_REQ) self._dump_restore(2, 1) grant.claims = { "userinfo": self.endpoint[1] .server_get("endpoint_context") - .claims_interface.get_claims( - session_id, scopes=_auth_req["scope"], usage="userinfo" - ) + .claims_interface.get_claims(session_id, scopes=_auth_req["scope"], usage="userinfo") } self._dump_restore(1, 2) - self.session_manager[2].set( - self.session_manager[2].decrypt_session_id(session_id), grant - ) + self.session_manager[2].set(self.session_manager[2].decrypt_session_id(session_id), grant) code = self._mint_code(grant, session_id, index=2) access_token = self._mint_access_token(grant, session_id, code, 2) self._dump_restore(2, 1) - http_info = { - "headers": {"authorization": "Bearer {}".format(access_token.value)} - } + http_info = {"headers": {"authorization": "Bearer {}".format(access_token.value)}} _req = self.endpoint[1].parse_request({}, http_info=http_info) args = self.endpoint[1].process_request(_req) @@ -485,3 +435,58 @@ def test_custom_scope(self): "eduperson_scoped_affiliation", } + def test_sman_db_integrity(self): + """ + this test assures that session database remains consistent after + - many consecutives flush + - deletion of key or salt + - some mess with values overwritten runtime + it show that flush and loads method will keep order, anyway. + """ + session_id = self._create_session(AUTH_REQ, index=1) + grant = ( + self.endpoint[1].server_get("endpoint_context").authz( + session_id, AUTH_REQ + ) + ) + sman = self.session_manager[1] + session_dump = sman.dump() + + # there after an exception a database could be inconsistent + # it would be better to always flush database when a new http request come + # and load session from previously loaded sessions + sman.flush() + # yes, two times to simulate those things that happens in real world + sman.flush() + + # check that a sman db schema is consistent after a flush + tdump = sman.dump() + for i in 'db', 'key', 'salt': + if i not in tdump: + raise ValueError( + f"{i} not found in session dump after a flush!" + ) + + # test that key and salt have not be touched after the flush + # they wouldn't change runtime (even if they are randomic). + for i in 'key', 'salt': + if session_dump[i] != tdump[i]: + raise ValueError( + f"Inconsistent Session schema dump after a flush. " + f"{i} has changed compared to which was configured." + ) + + # tests readonlyness of private attributes _key and _salt + for i in '_key', '_salt': + with pytest.raises(AttributeError): + setattr(sman, i, 'that thing') + + # ok, load the session and assert that everything is in the right place + # some mess before doing that + sman.key = 'ingoalla' + sman.salt = 'fantozzi' + + # ok, end of the games, session have been loaded and all the things be finally there! + sman.load(session_dump) + for i in 'db', 'key', 'salt': + assert session_dump[i] == sman.dump()[i] diff --git a/tests/test_60_dpop.py b/tests/test_60_dpop.py new file mode 100644 index 00000000..62af2b79 --- /dev/null +++ b/tests/test_60_dpop.py @@ -0,0 +1,266 @@ +import os + +from cryptojwt.jwk.ec import ECKey +from cryptojwt.jwk.ec import new_ec_key +from cryptojwt.jws.jws import factory +from cryptojwt.key_jar import init_key_jar +from oidcmsg.oauth2 import AccessTokenRequest +from oidcmsg.oauth2 import AuthorizationRequest +from oidcmsg.time_util import utc_time_sans_frac + +from oidcop.authn_event import create_authn_event +import pytest + +from oidcop import user_info +from oidcop.client_authn import verify_client +from oidcop.configure import OPConfiguration +from oidcop.oauth2.add_on.dpop import DPoPProof +from oidcop.oauth2.add_on.dpop import post_parse_request +from oidcop.oauth2.authorization import Authorization +from oidcop.oidc.token import Token +from oidcop.server import Server +from oidcop.user_authn.authn_context import INTERNETPROTOCOLPASSWORD + +DPOP_HEADER = ( + "eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMz" + "R0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFq" + "SG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwia" + "HRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwiaWF0IjoxNTYyMjY" + "yNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg" +) + + +def test_verify_header(): + _dpop = DPoPProof() + assert _dpop.verify_header(DPOP_HEADER) + assert set(_dpop.keys()) == {'typ', 'alg', 'jwk', 'jti', 'htm', 'htu', 'iat'} + assert _dpop.verify() is None + + _dpop_dict = _dpop.to_dict() + _dpop2 = DPoPProof().from_dict(_dpop_dict) + assert isinstance(_dpop2.key, ECKey) + + ec_key = new_ec_key(crv="P-256") + _dpop2.key = ec_key + _dpop2["jwk"] = ec_key.to_dict() + + _header = _dpop2.create_header() + + _dpop3 = DPoPProof() + assert _dpop3.verify_header(_header) + # should have the same content as _dpop only the key is different + + assert _dpop["htm"] == _dpop3["htm"] + + +KEYDEFS = [ + {"type": "RSA", "key": "", "use": ["sig"]}, + {"type": "EC", "crv": "P-256", "use": ["sig"]}, +] + +ISSUER = "https://example.com/" + +KEYJAR = init_key_jar(key_defs=KEYDEFS, issuer_id=ISSUER) +KEYJAR.import_jwks(KEYJAR.export_jwks(True, ISSUER), "") + +RESPONSE_TYPES_SUPPORTED = [ + ["code"], + ["token"], + ["id_token"], + ["code", "token"], + ["code", "id_token"], + ["id_token", "token"], + ["code", "token", "id_token"], + ["none"], +] + +CAPABILITIES = { + "response_types_supported": [" ".join(x) for x in RESPONSE_TYPES_SUPPORTED], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic", + "client_secret_jwt", + "private_key_jwt", + ], + "response_modes_supported": ["query", "fragment", "form_post"], + "subject_types_supported": ["public", "pairwise", "ephemeral"], + "grant_types_supported": [ + "authorization_code", + "implicit", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + ], + "claim_types_supported": ["normal", "aggregated", "distributed"], + "claims_parameter_supported": True, + "request_parameter_supported": True, + "request_uri_parameter_supported": True, +} + +AUTH_REQ = AuthorizationRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + scope=["openid"], + state="STATE", + response_type="code", +) + +TOKEN_REQ = AccessTokenRequest( + client_id="client_1", + redirect_uri="https://example.com/cb", + state="STATE", + grant_type="authorization_code", + client_secret="hemligt", +) + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +class TestEndpoint(object): + @pytest.fixture(autouse=True) + def create_endpoint(self): + conf = { + "issuer": ISSUER, + "password": "mycket hemligt", + "verify_ssl": False, + "capabilities": CAPABILITIES, + "add_on": { + "dpop": { + "function": "oidcop.oauth2.add_on.dpop.add_support", + "kwargs": { + "dpop_signing_alg_values_supported": ["ES256"] + } + }, + }, + "keys": {"uri_path": "jwks.json", "key_defs": KEYDEFS}, + "token_handler_args": { + "jwks_file": "private/token_jwks.json", + "code": {"lifetime": 600}, + "token": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": { + "lifetime": 3600, + "base_claims": {"eduperson_scoped_affiliation": None}, + "add_claims_by_scope": True, + "aud": ["https://example.org/appl"], + }, + }, + "refresh": { + "class": "oidcop.token.jwt_token.JWTToken", + "kwargs": {"lifetime": 3600, "aud": ["https://example.org/appl"], }, + }, + "id_token": { + "class": "oidcop.token.id_token.IDToken", + "kwargs": { + "base_claims": { + "email": {"essential": True}, + "email_verified": {"essential": True}, + } + }, + }, + }, + "endpoint": { + "authorization": { + "path": "{}/authorization", + "class": Authorization, + "kwargs": {}, + }, + "token": { + "path": "{}/token", + "class": Token, + "kwargs": {}}, + }, + "client_authn": verify_client, + "authentication": { + "anon": { + "acr": INTERNETPROTOCOLPASSWORD, + "class": "oidcop.user_authn.user.NoAuthn", + "kwargs": {"user": "diana"}, + } + }, + "template_dir": "template", + "userinfo": { + "class": user_info.UserInfo, + "kwargs": {"db_file": "users.json"}, + }, + } + server = Server(OPConfiguration(conf, base_path=BASEDIR), keyjar=KEYJAR) + self.endpoint_context = server.endpoint_context + self.endpoint_context.cdb["client_1"] = { + "client_secret": "hemligt", + "redirect_uris": [("https://example.com/cb", None)], + "client_salt": "salted", + "token_endpoint_auth_method": "client_secret_post", + "response_types": ["code", "token", "code id_token", "id_token"], + } + self.user_id = "diana" + self.token_endpoint = server.server_get("endpoint", "token") + self.session_manager = self.endpoint_context.session_manager + + def _create_session(self, auth_req, sub_type="public", sector_identifier=""): + if sector_identifier: + authz_req = auth_req.copy() + authz_req["sector_identifier_uri"] = sector_identifier + else: + authz_req = auth_req + client_id = authz_req["client_id"] + ae = create_authn_event(self.user_id) + return self.session_manager.create_session( + ae, authz_req, self.user_id, client_id=client_id, sub_type=sub_type + ) + + def _mint_code(self, grant, client_id): + session_id = self.session_manager.encrypted_session_id( + self.user_id, client_id, grant.id + ) + usage_rules = grant.usage_rules.get("authorization_code", {}) + _exp_in = usage_rules.get("expires_in") + + # Constructing an authorization code is now done + _code = grant.mint_token( + session_id=session_id, + endpoint_context=self.endpoint_context, + token_type="authorization_code", + token_handler=self.session_manager.token_handler["code"], + usage_rules=usage_rules, + ) + + if _exp_in: + if isinstance(_exp_in, str): + _exp_in = int(_exp_in) + if _exp_in: + _code.expires_at = utc_time_sans_frac() + _exp_in + return _code + + def test_post_parse_request(self): + auth_req = post_parse_request(AUTH_REQ, AUTH_REQ["client_id"], self.endpoint_context, + http_info={ + "headers": {"dpop": DPOP_HEADER}, + "url": 'https://server.example.com/token', + "method": "POST" + }) + assert auth_req + assert "dpop_jkt" in auth_req + + def test_process_request(self): + session_id = self._create_session(AUTH_REQ) + grant = self.session_manager[session_id] + code = self._mint_code(grant, AUTH_REQ["client_id"]) + + _token_request = TOKEN_REQ.to_dict() + _context = self.endpoint_context + _token_request["code"] = code.value + _req = self.token_endpoint.parse_request(_token_request, http_info={ + "headers": {"dpop": DPOP_HEADER}, + "url": 'https://server.example.com/token', + "method": "POST" + }) + + assert "dpop_jkt" in _req + + _resp = self.token_endpoint.process_request(request=_req) + assert _resp["response_args"]["token_type"] == "DPoP" + + access_token = _resp["response_args"]["access_token"] + jws = factory(access_token) + _payload = jws.jwt.payload() + assert "cnf" in _payload + assert _payload["cnf"]["jkt"] == _req["dpop_jkt"]