From f4b9bc12f1a21657b7445b47b0f63c32139d6f4f Mon Sep 17 00:00:00 2001 From: peppelinux Date: Mon, 9 Nov 2020 09:12:27 +0100 Subject: [PATCH 1/2] django-oidc-op moved to a separate repository --- README.md | 15 +- django_op/AUTHORS | 1 - django_op/LICENSE | 190 ------------ django_op/README.md | 142 +-------- django_op/example/data/oidc_op/certs/cert.pem | 31 -- django_op/example/data/oidc_op/certs/key.pem | 52 ---- .../example/data/oidc_rp/conf.django.yaml | 84 ----- django_op/example/example/__init__.py | 0 django_op/example/example/oidc_op.conf.yaml | 246 --------------- django_op/example/example/settings.py | 182 ----------- django_op/example/example/urls.py | 30 -- django_op/example/example/wsgi.py | 16 - django_op/example/manage.py | 21 -- django_op/example/oidc_op | 1 - django_op/example/requirements.txt | 1 - django_op/example/unical_accounts/__init__.py | 2 - django_op/example/unical_accounts/admin.py | 53 ---- .../example/unical_accounts/admin_inlines.py | 8 - django_op/example/unical_accounts/apps.py | 6 - django_op/example/unical_accounts/forms.py | 2 - .../migrations/0001_initial.py | 64 ---- .../unical_accounts/migrations/__init__.py | 0 django_op/example/unical_accounts/models.py | 82 ----- .../unical_accounts/templatetags/__init__.py | 0 .../unical_accounts/templatetags/has_group.py | 9 - django_op/example/unical_accounts/tests.py | 3 - django_op/example/unical_accounts/urls.py | 26 -- django_op/example/unical_accounts/views.py | 27 -- django_op/oidc_op/__init__.py | 4 - django_op/oidc_op/admin.py | 3 - django_op/oidc_op/application.py | 41 --- django_op/oidc_op/apps.py | 5 - django_op/oidc_op/configure.py | 65 ---- django_op/oidc_op/migrations/__init__.py | 0 django_op/oidc_op/models.py | 3 - django_op/oidc_op/tests.py | 3 - django_op/oidc_op/urls.py | 23 -- django_op/oidc_op/users.py | 146 --------- django_op/oidc_op/views.py | 289 ------------------ django_op/requirements.txt | 3 - 40 files changed, 6 insertions(+), 1873 deletions(-) delete mode 100644 django_op/AUTHORS delete mode 100644 django_op/LICENSE delete mode 100644 django_op/example/data/oidc_op/certs/cert.pem delete mode 100644 django_op/example/data/oidc_op/certs/key.pem delete mode 100644 django_op/example/data/oidc_rp/conf.django.yaml delete mode 100644 django_op/example/example/__init__.py delete mode 100644 django_op/example/example/oidc_op.conf.yaml delete mode 100644 django_op/example/example/settings.py delete mode 100644 django_op/example/example/urls.py delete mode 100644 django_op/example/example/wsgi.py delete mode 100755 django_op/example/manage.py delete mode 120000 django_op/example/oidc_op delete mode 100644 django_op/example/requirements.txt delete mode 100644 django_op/example/unical_accounts/__init__.py delete mode 100644 django_op/example/unical_accounts/admin.py delete mode 100644 django_op/example/unical_accounts/admin_inlines.py delete mode 100644 django_op/example/unical_accounts/apps.py delete mode 100644 django_op/example/unical_accounts/forms.py delete mode 100644 django_op/example/unical_accounts/migrations/0001_initial.py delete mode 100644 django_op/example/unical_accounts/migrations/__init__.py delete mode 100644 django_op/example/unical_accounts/models.py delete mode 100644 django_op/example/unical_accounts/templatetags/__init__.py delete mode 100644 django_op/example/unical_accounts/templatetags/has_group.py delete mode 100644 django_op/example/unical_accounts/tests.py delete mode 100644 django_op/example/unical_accounts/urls.py delete mode 100644 django_op/example/unical_accounts/views.py delete mode 100644 django_op/oidc_op/__init__.py delete mode 100644 django_op/oidc_op/admin.py delete mode 100644 django_op/oidc_op/application.py delete mode 100644 django_op/oidc_op/apps.py delete mode 100644 django_op/oidc_op/configure.py delete mode 100644 django_op/oidc_op/migrations/__init__.py delete mode 100644 django_op/oidc_op/models.py delete mode 100644 django_op/oidc_op/tests.py delete mode 100644 django_op/oidc_op/urls.py delete mode 100644 django_op/oidc_op/users.py delete mode 100644 django_op/oidc_op/views.py delete mode 100644 django_op/requirements.txt diff --git a/README.md b/README.md index 3141898c..caeb2797 100644 --- a/README.md +++ b/README.md @@ -118,21 +118,10 @@ git clone https://github.com/rohe/oidc-op.git ```` ##### Configure a Django OP -```` -cd oidc-op/django-oidc-op -pip install -r requirements.txt - -cd example -pip install -r requirements.txt - -./manage.py migrate -./manage.py createsuperuser -./manage.py collectstatic -gunicorn example.wsgi -b0.0.0.0:8000 --keyfile=./data/oidc_op/certs/key.pem --certfile=./data/oidc_op/certs/cert.pem --reload +See - -```` +https://github.com/peppelinux/django-oidc-op ##### Configure a Flask OP diff --git a/django_op/AUTHORS b/django_op/AUTHORS deleted file mode 100644 index b995f227..00000000 --- a/django_op/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -Giuseppe De Marco diff --git a/django_op/LICENSE b/django_op/LICENSE deleted file mode 100644 index fe2e6d0d..00000000 --- a/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/django_op/README.md b/django_op/README.md index 034bbaac..3fd068d2 100644 --- a/django_op/README.md +++ b/django_op/README.md @@ -1,140 +1,6 @@ -# django-oidc-op -A Django implementation of an **OIDC Provider** built top of [jwtconnect libraries](https://jwtconnect.io/). -If you are just going to build a standard OIDC Provider you only have to write the configuration file. - -This project is based on [Roland Hedberg's oidc-op](https://github.com/rohe/oidc-op). - -## Status -_Work in Progress_ - -Please wait for the first release tag before considering it ready to use. -Before adopting this project in a production use you should consider if the following endpoint should be enabled: - -- [Web Finger](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery) -- [dynamic discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) -- [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) - -**TODO**: _document how to disable them and how to register RP via django admin backend._ - -#### Endpoints - -Available resources are: - -- webfinger - - /.well-known/webfinger [to be tested] - -- provider_info - - /.well-known/openid-configuration - -- registration - - /registration - -- authorization - - /authorization - - authentication, which type decide to support, default: login form. - -- token - - access/authorization token - -- refresh_token - -- userinfo - - /userinfo - -- end_session - - logout - - -## 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 - -gunicorn example.wsgi -b0.0.0.0:8000 --keyfile=./data/oidc_op/certs/key.pem --certfile=./data/oidc_op/certs/cert.pem --reload -```` - -You can use [JWTConnect-Python-OidcRP](https://github.com/openid/JWTConnect-Python-OidcRP) as an example RP as follows: - -`RP_LOGFILE_NAME="./flrp.django.log" python3 -m flask_rp.wsgi ../django-oidc-op/example/data/oidc_rp/conf.django.yaml` - - -## Configure OIDC endpoint - -#### Django settings.py parameters - -`OIDC_OP_AUTHN_SALT_SIZE`: Salt size in byte, default: 4 (Integer). - -#### Signatures -These following files needed to be present in `data/oidc_op/private`. - -1. session.json (JWK symmetric); -2. cookie_sign_jwk.json (JWK symmetric); -3. 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 -```` - -## General description - -The example included in this project enables dynamic registration of RPs (you can even disable it). -Using an example RP like [JWTConnect-Python-OidcRP](https://github.com/openid/JWTConnect-Python-OidcRP) -and configuring in `CLIENTS` section to use django-oidc-op (see `example/data/oidc_rp/conf.django.yaml`), -we'll see the following flow happens: - -1. /.well-known/openid-configuration - RP get the Provider configuration, what declared in the configuration at `op.server_info`; -2. /registration - RP registers in the Provider if `dynamic client registration` is enabled (default true) -3. /authorization - RP mades OIDC authorization -4. RP going to be redirected to login form page (see authn_methods.py) -5. user-agent posts form (user credentials) to `/verify/user_pass_django` -6. verify_user in django, on top of oidcendpoint_app.endpoint_context.authn_broker -7. RP request for an access token -> the response of the previous authentication is a HttpRedirect to op's /token resource -8. RP get the redirection to OP's USERINFO endpoint, using the access token got before - - -## UserInfo endpoint - -Claims to be released are configured in `op.server_info.user_info` (in `conf.yaml`). -All 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 -```` - -**TODO**: Do a RP configuration UI for custom claims release for every client. - - -## OIDC endpoint url prefix -Can be configured in `urls.py` and also in oidc_op `conf.yaml`. - -- /oidc/endpoint/ - +djangoioidc-op +-------------- +Moved permanently to: +https://github.com/peppelinux/django-oidc-op diff --git a/django_op/example/data/oidc_op/certs/cert.pem b/django_op/example/data/oidc_op/certs/cert.pem deleted file mode 100644 index 210fd240..00000000 --- a/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/django_op/example/data/oidc_op/certs/key.pem b/django_op/example/data/oidc_op/certs/key.pem deleted file mode 100644 index 15449664..00000000 --- a/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/django_op/example/data/oidc_rp/conf.django.yaml b/django_op/example/data/oidc_rp/conf.django.yaml deleted file mode 100644 index f5242b4b..00000000 --- a/django_op/example/data/oidc_rp/conf.django.yaml +++ /dev/null @@ -1,84 +0,0 @@ -PORT: 8099 -BASEURL: "https://127.0.0.1:8099" - -# If BASE is https these has to be specified -SERVER_CERT: "certs/cert.pem" -SERVER_KEY: "certs/key.pem" -CA_BUNDLE: '' - -# This is just for testing an local usage. In all other cases it MUST be True -VERIFY_SSL: false - -KEYDEFS: &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 - -# PUBLIC_JWKS_PATH: 'https://127.0.0.1:8090/static/jwks.json' -# PRIVATE_JWKS_PATH: './private/jwks.json' - -client_preferences: &id001 - 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: &id002 - discovery: - class: oidcservice.oidc.provider_info_discovery.ProviderInfoDiscovery - kwargs: {} - registration: - class: oidcservice.oidc.registration.Registration - kwargs: {} - authorization: - class: oidcservice.oidc.authorization.Authorization - kwargs: {} - accesstoken: - class: oidcservice.oidc.access_token.AccessToken - kwargs: {} - refresh_accesstoken: - class: oidcservice.oidc.refresh_access_token.RefreshAccessToken - kwargs: {} - userinfo: - class: oidcservice.oidc.userinfo.UserInfo - kwargs: {} - end_session: - class: oidcservice.oidc.end_session.EndSession - kwargs: {} - - -CLIENTS: - django_oidc_op: - client_preferences: *id001 - issuer: https://127.0.0.1:8000/ - jwks_uri: https://127.0.0.1:8099/static/jwks.json - redirect_uris: ['https://127.0.0.1:8099/authz_cb/django_oidc_op'] - services: *id002 - add_ons: - pkce: - function: oidcservice.oidc.add_on.pkce.add_pkce_support - kwargs: - code_challenge_length: 64 - code_challenge_method: S256 - -# Whether an attempt to fetch the userinfo should be made -USERINFO: true diff --git a/django_op/example/example/__init__.py b/django_op/example/example/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django_op/example/example/oidc_op.conf.yaml b/django_op/example/example/oidc_op.conf.yaml deleted file mode 100644 index 24f8efaa..00000000 --- a/django_op/example/example/oidc_op.conf.yaml +++ /dev/null @@ -1,246 +0,0 @@ -# TODO: the following four should be handled in settings.py -session_cookie_name: django_oidc_op -port: &port 8000 -domain: &domain 127.0.0.1 -server_name: &server_name 127.0.0.1:8000 -base_url: &base_url https://127.0.0.1:8000 - -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' - -session_jwk: 'data/oidc_op/private/session.json' - -op: - server_info: - issuer: *base_url - verify_ssl: False - capabilities: - 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 - subject_types_supported: - - public - - pairwise - grant_types_supported: - - authorization_code - - implicit - - urn:ietf:params:oauth:grant-type:jwt-bearer - - refresh_token - # TODO: Django mapping of claims -> user attrs - claim_types_supported: - - normal - - aggregated - - distributed - # filter as you prefer, see oidcmsg(.__init__.)SCOPE2CLAIMS - # defaults are ['family_name', 'email_verified', 'profile', 'given_name', 'picture', 'address', 'name', 'sub', 'email', 'zoneinfo', 'gender', 'website', 'phone_number_verified', 'middle_name', 'nickname', 'birthdate', 'preferred_username', 'locale', 'updated_at', 'phone_number'] - # claims_supported: - # - picture - # - nickname - claims_parameter_supported: True - request_parameter_supported: True - request_uri_parameter_supported: True - frontchannel_logout_supported: True - frontchannel_logout_session_supported: True - backchannel_logout_supported: True - backchannel_logout_session_supported: True - check_session_iframe: https://127.0.0.1:5000/check_session_iframe - id_token: - class: oidcendpoint.id_token.IDToken - kwargs: - default_claims: - email: - essential: True - email_verified: - essential: True - token_handler_args: - jwks_def: - private_path: data/oidc_op/private/token_jwk.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.jwt_token.JWTToken - lifetime: 3600 - add_claims: - - email - - email_verified - - phone_number - - phone_number_verified - add_claim_by_scope: True - aud: - - https://127.0.0.1:8000 - refresh: - lifetime: 86400 - jwks: - *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 - 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: - # TODO: make a authn method for 'client_authn_method' - client_authn_method: null - token: - path: token - class: oidcendpoint.oidc.token.AccessToken - kwargs: - client_authn_method: - - client_secret_post - - client_secret_basic - - client_secret_jwt - - private_key_jwt - userinfo: - path: userinfo - class: oidcendpoint.oidc.userinfo.UserInfo - end_session: - path: session - class: oidcendpoint.oidc.session.Session - kwargs: - logout_verify_url: verify_logout - post_logout_uri_path: post_logout - signing_alg: "ES256" - 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: - 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: *domain - 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 if needed - add_on: - pkce: - function: oidcendpoint.oidc.add_on.pkce.add_pkce_support - kwargs: - essential: True - code_challenge_method: - #plain - S256 - S384 - S512 diff --git a/django_op/example/example/settings.py b/django_op/example/example/settings.py deleted file mode 100644 index 980b5f59..00000000 --- a/django_op/example/example/settings.py +++ /dev/null @@ -1,182 +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 = [ - 'unical_accounts', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'oidc_op', - -] - -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 = "unical_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') - -OIDCENDPOINT_CONFIG = 'example/oidc_op.conf.yaml' - -# LOGGING -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'default': { - # 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': 'INFO', - 'class': 'logging.StreamHandler', - }, - }, - 'loggers': { - 'django': { - 'handlers': ['console', 'mail_admins'], - 'level': 'ERROR', - 'propagate': False, - }, - 'oidc_op': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': False, - }, - 'oidcendpoint': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': False, - }, - 'oidcmsg': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': False, - }, - } -} diff --git a/django_op/example/example/urls.py b/django_op/example/example/urls.py deleted file mode 100644 index 1d9a86ed..00000000 --- a/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_op' in settings.INSTALLED_APPS: - import oidc_op.urls - urlpatterns += path('', include((oidc_op.urls, 'oidc_op',))), diff --git a/django_op/example/example/wsgi.py b/django_op/example/example/wsgi.py deleted file mode 100644 index f18f865c..00000000 --- a/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/django_op/example/manage.py b/django_op/example/manage.py deleted file mode 100755 index 2f9e225a..00000000 --- a/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/django_op/example/oidc_op b/django_op/example/oidc_op deleted file mode 120000 index 108427ed..00000000 --- a/django_op/example/oidc_op +++ /dev/null @@ -1 +0,0 @@ -../oidc_op/ \ No newline at end of file diff --git a/django_op/example/requirements.txt b/django_op/example/requirements.txt deleted file mode 100644 index fbb2b950..00000000 --- a/django_op/example/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pycountry diff --git a/django_op/example/unical_accounts/__init__.py b/django_op/example/unical_accounts/__init__.py deleted file mode 100644 index a4956ea5..00000000 --- a/django_op/example/unical_accounts/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -default_app_config = 'unical_accounts.apps.Unical_AccountsConfig' - diff --git a/django_op/example/unical_accounts/admin.py b/django_op/example/unical_accounts/admin.py deleted file mode 100644 index 08ca4e8d..00000000 --- a/django_op/example/unical_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/django_op/example/unical_accounts/admin_inlines.py b/django_op/example/unical_accounts/admin_inlines.py deleted file mode 100644 index 33b0047c..00000000 --- a/django_op/example/unical_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/django_op/example/unical_accounts/apps.py b/django_op/example/unical_accounts/apps.py deleted file mode 100644 index 3b5103e3..00000000 --- a/django_op/example/unical_accounts/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class Unical_AccountsConfig(AppConfig): - name = 'unical_accounts' - verbose_name = "Autenticazione e Autorizzazione Utenti" diff --git a/django_op/example/unical_accounts/forms.py b/django_op/example/unical_accounts/forms.py deleted file mode 100644 index 95ec770d..00000000 --- a/django_op/example/unical_accounts/forms.py +++ /dev/null @@ -1,2 +0,0 @@ -from django import forms -from .models import * diff --git a/django_op/example/unical_accounts/migrations/0001_initial.py b/django_op/example/unical_accounts/migrations/0001_initial.py deleted file mode 100644 index 6e41e8b7..00000000 --- a/django_op/example/unical_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/django_op/example/unical_accounts/migrations/__init__.py b/django_op/example/unical_accounts/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django_op/example/unical_accounts/models.py b/django_op/example/unical_accounts/models.py deleted file mode 100644 index 2fa08c1f..00000000 --- a/django_op/example/unical_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=30, - 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/django_op/example/unical_accounts/templatetags/__init__.py b/django_op/example/unical_accounts/templatetags/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django_op/example/unical_accounts/templatetags/has_group.py b/django_op/example/unical_accounts/templatetags/has_group.py deleted file mode 100644 index aa402dab..00000000 --- a/django_op/example/unical_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/django_op/example/unical_accounts/tests.py b/django_op/example/unical_accounts/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/django_op/example/unical_accounts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/django_op/example/unical_accounts/urls.py b/django_op/example/unical_accounts/urls.py deleted file mode 100644 index 7a56fbd5..00000000 --- a/django_op/example/unical_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="unical_accounts" - -urlpatterns = [ - - # url(r'^login/$', Login, name='login'), - # path('logout', Logout, name='logout'), - -] diff --git a/django_op/example/unical_accounts/views.py b/django_op/example/unical_accounts/views.py deleted file mode 100644 index 3050ccaf..00000000 --- a/django_op/example/unical_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/django_op/oidc_op/__init__.py b/django_op/oidc_op/__init__.py deleted file mode 100644 index 154a4637..00000000 --- a/django_op/oidc_op/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . application import oidcendpoint_application - - -oidcendpoint_app = oidcendpoint_application() diff --git a/django_op/oidc_op/admin.py b/django_op/oidc_op/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/django_op/oidc_op/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/django_op/oidc_op/application.py b/django_op/oidc_op/application.py deleted file mode 100644 index 8b7183d5..00000000 --- a/django_op/oidc_op/application.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from cryptojwt.key_jar import init_key_jar -from django.conf import settings -from oidcendpoint.endpoint_context import EndpointContext -from urllib.parse import urlparse - -from . configure import Configuration - - -def init_oidc_op_endpoints(app): - _config = app.srv_config.op - _server_info_config = _config['server_info'] - - _kj_args = {k:v for k,v in _server_info_config['jwks'].items() - if k != 'uri_path'} - _kj = init_key_jar(**_kj_args) - iss = _server_info_config['issuer'] - - # make sure I have a set of keys under my 'real' name - _kj.import_jwks_as_json(_kj.export_jwks_as_json(True, ''), iss) - _kj.verify_ssl = _config['server_info'].get('verify_ssl', False) - - endpoint_context = EndpointContext(_server_info_config, keyjar=_kj, - cwd=settings.BASE_DIR) - - return endpoint_context - - -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 oidcendpoint_application(config_file=settings.OIDCENDPOINT_CONFIG): - config = Configuration.create_from_config_file(config_file) - app = oidc_provider_init_app(config) - return app diff --git a/django_op/oidc_op/apps.py b/django_op/oidc_op/apps.py deleted file mode 100644 index 28699aa0..00000000 --- a/django_op/oidc_op/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class OidcOpConfig(AppConfig): - name = 'oidc_op' diff --git a/django_op/oidc_op/configure.py b/django_op/oidc_op/configure.py deleted file mode 100644 index 2b5236fe..00000000 --- a/django_op/oidc_op/configure.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Configuration management for IDP""" - -import json -import os -import sys -from typing import Dict - -from cryptojwt.jwk.hmac import SYMKey -from cryptojwt.jwx import key_from_jwk_dict -from django.conf import settings - -from oidcop.logging import configure_logging -from oidcop.utils import load_yaml_config - -# TODO: check this -try: - from secrets import token_urlsafe as rnd_token -except ImportError: - from oidcendpoint import rndstr as rnd_token - - -class Configuration: - """OP Configuration""" - - def __init__(self, conf: Dict) -> None: - self.logger = configure_logging(settings.LOGGING) - - # OIDC provider configuration - self.conf = conf - self.op = conf.get('op') - - # TODO: here manage static clients without dyn registration enabled - # self.oidc_clients = conf.get('oidc_clients', {}) - - # session key - self.session_jwk = conf.get('session_jwk') - if self.session_jwk is not None: - self.logger.debug("Reading session signer from %s", self.session_jwk) - try: - with open(self.session_jwk) as jwk_file: - jwk_dict = json.loads(jwk_file.read()) - self.session_key = key_from_jwk_dict(jwk_dict).k - except Exception: - self.logger.critical("Failed reading session signer from %s", - self.session_jwk) - sys.exit(-1) - else: - self.logger.debug("Generating random session signer") - self.session_key = SYMKey(key=rnd_token(32)).k - - # set OP session key - if self.op is not None: - if self.op['server_info'].get('password') is None: - key = self.session_key - self.op['server_info']['password'] = key - self.logger.debug("Set server password to %s", key) - - # templates environment - self.template_dir = os.path.abspath(conf.get('template_dir', 'templates')) - - - @classmethod - def create_from_config_file(cls, filename: str): - """Load configuration as YAML""" - return cls(load_yaml_config(filename)) diff --git a/django_op/oidc_op/migrations/__init__.py b/django_op/oidc_op/migrations/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/django_op/oidc_op/models.py b/django_op/oidc_op/models.py deleted file mode 100644 index 71a83623..00000000 --- a/django_op/oidc_op/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/django_op/oidc_op/tests.py b/django_op/oidc_op/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/django_op/oidc_op/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/django_op/oidc_op/urls.py b/django_op/oidc_op/urls.py deleted file mode 100644 index 04a01de1..00000000 --- a/django_op/oidc_op/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'oidc_op' - -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('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/django_op/oidc_op/users.py b/django_op/oidc_op/users.py deleted file mode 100644 index dbc4c331..00000000 --- a/django_op/oidc_op/users.py +++ /dev/null @@ -1,146 +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 oidcendpoint.util import instantiate -from oidcendpoint.user_authn.user import (create_signed_jwt, - verify_signed_jwt) -from oidcendpoint.user_authn.user import UserAuthnMethod - - -class UserPassDjango(UserAuthnMethod): - """ - see oidcendpoint.authn_context - oidcendpoint.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", - endpoint_context=None, verify_endpoint='', **kwargs): - """ - template_handler is only for backwards compatibility - it will be always replaced by Django's default - """ - super(UserPassDjango, self).__init__(endpoint_context=endpoint_context) - - 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.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) - try: - _kwargs[_uri] = kwargs[_uri] - except KeyError: - pass - else: - _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[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 {} - - try: - return self.filter(user, user_info_claims) - except KeyError: - return {} - - 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/django_op/oidc_op/views.py b/django_op/oidc_op/views.py deleted file mode 100644 index c86c6cec..00000000 --- a/django_op/oidc_op/views.py +++ /dev/null @@ -1,289 +0,0 @@ -import base64 -import logging -import json -import os - -from django.conf import settings -from django.http import (HttpResponse, - HttpResponseBadRequest, - HttpResponseRedirect, - JsonResponse) -from django.views.decorators.csrf import csrf_exempt -from django.shortcuts import render, render_to_response -from django.urls import reverse -from django.utils.translation import gettext as _ -from oidcendpoint.authn_event import create_authn_event -from oidcendpoint.exception import FailedAuthentication -from oidcendpoint.oidc.token import AccessToken -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 oidc_op import oidcendpoint_app - - -logger = logging.getLogger(__name__) - - -def _add_cookie(resp, cookie_spec): - for key, _morsel in cookie_spec.items(): - kwargs = {'value': _morsel.value} - for param in ['expires', 'path', 'comment', 'domain', 'max-age', - 'secure', - 'version']: - if _morsel[param]: - kwargs[param] = _morsel[param] - resp.set_cookie(key, **kwargs) - - -def add_cookie(resp, cookie_spec): - if isinstance(cookie_spec, list): - for _spec in cookie_spec: - _add_cookie(resp, _spec) - - -def do_response(endpoint, req_args, error='', **args): - info = endpoint.do_response(request=req_args, error=error, **args) - - logger = oidcendpoint_app.srv_config.logger - logger.debug('do_response: {}'.format(info)) - - try: - _response_placement = info['response_placement'] - except KeyError: - _response_placement = endpoint.response_placement - - logger.debug('response_placement: {}'.format(_response_placement)) - - if error: - if _response_placement == 'body': - logger.info('Error Response: {}'.format(info['response'])) - resp = HttpResponse(info['response'], status=400) - else: # _response_placement == 'url': - logger.info('Redirect to: {}'.format(info['response'])) - resp = HttpResponseRedirect(info['response']) - else: - if _response_placement == 'body': - logger.info('Response: {}'.format(info['response'])) - resp = HttpResponse(info['response'], status=200) - else: # _response_placement == 'url': - logger.info('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 service_endpoint(request, endpoint): - """ - TODO: documentation here - """ - logger = oidcendpoint_app.srv_config.logger - logger.info('At the "{}" endpoint'.format(endpoint.endpoint_name)) - - # if hasattr(request, 'debug') and request.debug: - # import pdb; pdb.set_trace() - - authn = request.headers.get('Authorization', {}) - pr_args = {'auth': authn} - if authn: - logger.debug('request.headers["Authorization"] => {}'.format(pr_args)) - - 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 authn: - 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()} - - # for .well-known resources like provider-config no data are submitted - # if not data: - # ... not possible in this implementation - - logger.debug('Request arguments [{}]: {}'.format(request.method, data)) - try: - req_args = endpoint.parse_request(data, **pr_args) - except Exception as err: - logger.error(err) - return JsonResponse(json.dumps({ - 'error': 'invalid_request', - 'error_description': str(err), - 'method': request.method - }), status=400) - - logger.info('request: {}'.format(req_args)) - if isinstance(req_args, ResponseMessage) and 'error' in req_args: - return JsonResponse(req_args.__dict__, status=400) - - if request.COOKIES: - logger.debug(request.COOKIES) - # TODO: cookie - kwargs = {'cookie': request.COOKIES} - else: - kwargs = {} - - try: - if isinstance(endpoint, AccessToken): - args = endpoint.process_request(AccessTokenRequest(**req_args), - **kwargs) - else: - args = endpoint.process_request(req_args, **kwargs) - except Exception as err: - message = '{}'.format(err) - logger.error(message) - return JsonResponse(json.dumps({ - 'error': 'invalid_request', - 'error_description': str(err) - }), status=400) - - logger.info('Response args: {}'.format(args)) - 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 = oidcendpoint_app.endpoint_context.endpoint['provider_config'] - # TODO - # if service == 'openid-federation': - # _endpoint = oidcendpoint_app.endpoint_context.endpoint['provider_info'] - elif service == 'webfinger': - _endpoint = oidcendpoint_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 = oidcendpoint_app.endpoint_context.endpoint['registration'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def registration_api(): - logger.info('registration API') - return service_endpoint(request, - oidcendpoint_app.endpoint_context.endpoint['registration_api']) - - -def authorization(request): - _endpoint = oidcendpoint_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 = oidcendpoint_app.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 = oidcendpoint_app.endpoint_context.endpoint['authorization'] - args = endpoint.authz_part2(user=user.username, request=authz_request, - authn_event=authn_event) - - 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 = oidcendpoint_app.endpoint_context.endpoint['token'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def userinfo(request): - logger.info('userinfo request') - _endpoint = oidcendpoint_app.endpoint_context.endpoint['userinfo'] - # if not hasattr(request, 'debug'): - # request.debug = 0 - # request.debug +=1 - return service_endpoint(request, _endpoint) - - -######## -# LOGOUT -######## -def session_endpoint(request): - return service_endpoint(request, - oidcendpoint_app.endpoint_context.endpoint['session']) - -@csrf_exempt -def rp_logout(request): - _endp = oidcendpoint_app.endpoint_context.endpoint['session'] - _info = _endp.unpack_signed_jwt(request.POST['sjwt']) - alla = 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_to_response('frontchannel_logout.html', d) - - else: - res = HttpResponseRedirect(_info['redirect_uri']) - _kakor = _endp.kill_cookies() - _add_cookie(res, _kakor) - - return res - -def verify_logout(request): - part = urlparse(oidcendpoint_app.endpoint_context.issuer) - d = dict(op=part.hostname, - do_logout='rp_logout', - sjwt=request.GET['sjwt'] or request.POST['sjwt']) - return render_to_response('logout.html', d) - - -def post_logout(request): - return render_to_response('post_logout.html') diff --git a/django_op/requirements.txt b/django_op/requirements.txt deleted file mode 100644 index 64166d39..00000000 --- a/django_op/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django>=2.2.4 -oidcop -gunicorn From 6793e3a415f95b246e3306de93d550dce037b778 Mon Sep 17 00:00:00 2001 From: peppelinux Date: Thu, 29 Apr 2021 18:12:50 +0200 Subject: [PATCH 2/2] fix: django_op example update --- example/django_op/README.md | 132 ++-- example/django_op/example/.coveragerc | 9 + .../django_op/example/accounts/__init__.py | 2 + .../{unical_accounts => accounts}/admin.py | 0 .../admin_inlines.py | 0 .../{unical_accounts => accounts}/apps.py | 4 +- .../{unical_accounts => accounts}/forms.py | 0 .../migrations/0001_initial.py | 0 .../migrations/0002_auto_20191202_1526.py | 18 + .../migrations/__init__.py | 0 .../{unical_accounts => accounts}/models.py | 2 +- .../templatetags/__init__.py | 0 .../templatetags/has_group.py | 0 .../{unical_accounts => accounts}/tests.py | 0 .../{unical_accounts => accounts}/urls.py | 4 +- .../{unical_accounts => accounts}/views.py | 0 .../example/data/oidc_rp/conf.django.yaml | 149 +++-- .../django_op/example/data/oidc_rp/conf.json | 322 ++++++++++ .../example/example/oidc_op.conf.yaml | 231 ++++--- .../example/example/oidc_provider_settings.py | 367 +++++++++++ example/django_op/example/example/settings.py | 66 +- example/django_op/example/example/urls.py | 6 +- example/django_op/example/oidc_op | 1 - example/django_op/example/oidc_provider | 1 + example/django_op/example/run.bash | 7 + .../example/unical_accounts/__init__.py | 2 - example/django_op/oidc-op.dev.notes.md | 260 ++++++++ example/django_op/oidc_op/__init__.py | 4 - example/django_op/oidc_op/admin.py | 3 - example/django_op/oidc_op/application.py | 41 -- example/django_op/oidc_op/apps.py | 5 - example/django_op/oidc_op/configure.py | 65 -- example/django_op/oidc_op/models.py | 3 - example/django_op/oidc_op/tests.py | 3 - example/django_op/oidc_op/urls.py | 23 - example/django_op/oidc_op/views.py | 289 --------- example/django_op/oidc_provider/__init__.py | 1 + example/django_op/oidc_provider/admin.py | 214 +++++++ .../django_op/oidc_provider/application.py | 49 ++ example/django_op/oidc_provider/apps.py | 6 + .../oidc_provider/migrations/0001_initial.py | 155 +++++ .../migrations/__init__.py | 0 example/django_op/oidc_provider/models.py | 582 ++++++++++++++++++ example/django_op/oidc_provider/tests.py | 2 + .../oidc_provider/tests/01_client_db.py | 102 +++ .../django_op/oidc_provider/tests/__init__.py | 0 example/django_op/oidc_provider/urls.py | 33 + .../{oidc_op => oidc_provider}/users.py | 45 +- example/django_op/oidc_provider/utils.py | 31 + example/django_op/oidc_provider/views.py | 351 +++++++++++ example/django_op/requirements.txt | 9 +- example/django_op/snippets/db_interfaces.py | 287 +++++++++ example/django_op/snippets/msg_test.py | 39 ++ example/django_op/snippets/rp_handler.py | 178 ++++++ example/flask_op/run.sh | 3 + 55 files changed, 3441 insertions(+), 665 deletions(-) create mode 100644 example/django_op/example/.coveragerc create mode 100644 example/django_op/example/accounts/__init__.py rename example/django_op/example/{unical_accounts => accounts}/admin.py (100%) rename example/django_op/example/{unical_accounts => accounts}/admin_inlines.py (100%) rename example/django_op/example/{unical_accounts => accounts}/apps.py (58%) rename example/django_op/example/{unical_accounts => accounts}/forms.py (100%) rename example/django_op/example/{unical_accounts => accounts}/migrations/0001_initial.py (100%) create mode 100644 example/django_op/example/accounts/migrations/0002_auto_20191202_1526.py rename example/django_op/example/{unical_accounts => accounts}/migrations/__init__.py (100%) rename example/django_op/example/{unical_accounts => accounts}/models.py (99%) rename example/django_op/example/{unical_accounts => accounts}/templatetags/__init__.py (100%) rename example/django_op/example/{unical_accounts => accounts}/templatetags/has_group.py (100%) rename example/django_op/example/{unical_accounts => accounts}/tests.py (100%) rename example/django_op/example/{unical_accounts => accounts}/urls.py (96%) rename example/django_op/example/{unical_accounts => accounts}/views.py (100%) create mode 100644 example/django_op/example/data/oidc_rp/conf.json create mode 100644 example/django_op/example/example/oidc_provider_settings.py delete mode 120000 example/django_op/example/oidc_op create mode 120000 example/django_op/example/oidc_provider create mode 100644 example/django_op/example/run.bash delete mode 100644 example/django_op/example/unical_accounts/__init__.py create mode 100644 example/django_op/oidc-op.dev.notes.md delete mode 100644 example/django_op/oidc_op/__init__.py delete mode 100644 example/django_op/oidc_op/admin.py delete mode 100644 example/django_op/oidc_op/application.py delete mode 100644 example/django_op/oidc_op/apps.py delete mode 100644 example/django_op/oidc_op/configure.py delete mode 100644 example/django_op/oidc_op/models.py delete mode 100644 example/django_op/oidc_op/tests.py delete mode 100644 example/django_op/oidc_op/urls.py delete mode 100644 example/django_op/oidc_op/views.py create mode 100644 example/django_op/oidc_provider/__init__.py create mode 100644 example/django_op/oidc_provider/admin.py create mode 100644 example/django_op/oidc_provider/application.py create mode 100644 example/django_op/oidc_provider/apps.py create mode 100644 example/django_op/oidc_provider/migrations/0001_initial.py rename example/django_op/{oidc_op => oidc_provider}/migrations/__init__.py (100%) create mode 100644 example/django_op/oidc_provider/models.py create mode 100644 example/django_op/oidc_provider/tests.py create mode 100644 example/django_op/oidc_provider/tests/01_client_db.py create mode 100644 example/django_op/oidc_provider/tests/__init__.py create mode 100644 example/django_op/oidc_provider/urls.py rename example/django_op/{oidc_op => oidc_provider}/users.py (78%) create mode 100644 example/django_op/oidc_provider/utils.py create mode 100644 example/django_op/oidc_provider/views.py create mode 100644 example/django_op/snippets/db_interfaces.py create mode 100644 example/django_op/snippets/msg_test.py create mode 100644 example/django_op/snippets/rp_handler.py create mode 100644 example/flask_op/run.sh diff --git a/example/django_op/README.md b/example/django_op/README.md index 034bbaac..a03cea12 100644 --- a/example/django_op/README.md +++ b/example/django_op/README.md @@ -1,49 +1,39 @@ # django-oidc-op -A Django implementation of an **OIDC Provider** built top of [jwtconnect libraries](https://jwtconnect.io/). -If you are just going to build a standard OIDC Provider you only have to write the configuration file. +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: -## Status -_Work in Progress_ - -Please wait for the first release tag before considering it ready to use. -Before adopting this project in a production use you should consider if the following endpoint should be enabled: - -- [Web Finger](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery) -- [dynamic discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) -- [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html) - -**TODO**: _document how to disable them and how to register RP via django admin backend._ - -#### Endpoints +- [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) -Available resources are: +It also supports the followings `add_ons` modules. -- webfinger - - /.well-known/webfinger [to be tested] +- 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) -- provider_info - - /.well-known/openid-configuration - -- registration - - /registration - -- authorization - - /authorization - - authentication, which type decide to support, default: login form. +## Status -- token - - access/authorization token +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. -- refresh_token -- userinfo - - /userinfo +Works: -- end_session - - logout +- 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 @@ -59,26 +49,37 @@ pip install -r requirements.txt ./manage.py createsuperuser ./manage.py collectstatic -gunicorn example.wsgi -b0.0.0.0:8000 --keyfile=./data/oidc_op/certs/key.pem --certfile=./data/oidc_op/certs/cert.pem --reload +## 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 an example RP as follows: +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 +``` -`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). -#### Signatures -These following files needed to be present in `data/oidc_op/private`. +#### 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); -2. cookie_sign_jwk.json (JWK symmetric); -3. cookie_enc_jwk.json (JWK symmetric), optional, see `conf.yaml`. + +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. @@ -88,30 +89,19 @@ A JWK creation example would be: jwkgen --kty SYM > data/oidc_op/private/cookie_enc_jwk.json ```` -## General description - -The example included in this project enables dynamic registration of RPs (you can even disable it). -Using an example RP like [JWTConnect-Python-OidcRP](https://github.com/openid/JWTConnect-Python-OidcRP) -and configuring in `CLIENTS` section to use django-oidc-op (see `example/data/oidc_rp/conf.django.yaml`), -we'll see the following flow happens: +## Django specific implementation -1. /.well-known/openid-configuration - RP get the Provider configuration, what declared in the configuration at `op.server_info`; -2. /registration - RP registers in the Provider if `dynamic client registration` is enabled (default true) -3. /authorization - RP mades OIDC authorization -4. RP going to be redirected to login form page (see authn_methods.py) -5. user-agent posts form (user credentials) to `/verify/user_pass_django` -6. verify_user in django, on top of oidcendpoint_app.endpoint_context.authn_broker -7. RP request for an access token -> the response of the previous authentication is a HttpRedirect to op's /token resource -8. RP get the redirection to OP's USERINFO endpoint, using the access token got before +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 +#### UserInfo endpoint Claims to be released are configured in `op.server_info.user_info` (in `conf.yaml`). -All the attributes release and user authentication mechanism rely on classes implemented in `oidc_op.users.py`. +The attributes release and user authentication mechanism rely on classes implemented in `oidc_op.users.py`. Configuration Example: @@ -128,8 +118,16 @@ Configuration Example: verified_email: email ```` -**TODO**: Do a RP configuration UI for custom claims release for every client. +#### 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`. @@ -137,4 +135,16 @@ 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 +```` diff --git a/example/django_op/example/.coveragerc b/example/django_op/example/.coveragerc new file mode 100644 index 00000000..7fe59bb4 --- /dev/null +++ b/example/django_op/example/.coveragerc @@ -0,0 +1,9 @@ +[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 new file mode 100644 index 00000000..b48c1ec8 --- /dev/null +++ b/example/django_op/example/accounts/__init__.py @@ -0,0 +1,2 @@ +default_app_config = 'accounts.apps.AccountsConfig' + diff --git a/example/django_op/example/unical_accounts/admin.py b/example/django_op/example/accounts/admin.py similarity index 100% rename from example/django_op/example/unical_accounts/admin.py rename to example/django_op/example/accounts/admin.py diff --git a/example/django_op/example/unical_accounts/admin_inlines.py b/example/django_op/example/accounts/admin_inlines.py similarity index 100% rename from example/django_op/example/unical_accounts/admin_inlines.py rename to example/django_op/example/accounts/admin_inlines.py diff --git a/example/django_op/example/unical_accounts/apps.py b/example/django_op/example/accounts/apps.py similarity index 58% rename from example/django_op/example/unical_accounts/apps.py rename to example/django_op/example/accounts/apps.py index 3b5103e3..e4b019ea 100644 --- a/example/django_op/example/unical_accounts/apps.py +++ b/example/django_op/example/accounts/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class Unical_AccountsConfig(AppConfig): - name = 'unical_accounts' +class AccountsConfig(AppConfig): + name = 'accounts' verbose_name = "Autenticazione e Autorizzazione Utenti" diff --git a/example/django_op/example/unical_accounts/forms.py b/example/django_op/example/accounts/forms.py similarity index 100% rename from example/django_op/example/unical_accounts/forms.py rename to example/django_op/example/accounts/forms.py diff --git a/example/django_op/example/unical_accounts/migrations/0001_initial.py b/example/django_op/example/accounts/migrations/0001_initial.py similarity index 100% rename from example/django_op/example/unical_accounts/migrations/0001_initial.py rename to example/django_op/example/accounts/migrations/0001_initial.py 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 new file mode 100644 index 00000000..57e6923c --- /dev/null +++ b/example/django_op/example/accounts/migrations/0002_auto_20191202_1526.py @@ -0,0 +1,18 @@ +# 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/unical_accounts/migrations/__init__.py b/example/django_op/example/accounts/migrations/__init__.py similarity index 100% rename from example/django_op/example/unical_accounts/migrations/__init__.py rename to example/django_op/example/accounts/migrations/__init__.py diff --git a/example/django_op/example/unical_accounts/models.py b/example/django_op/example/accounts/models.py similarity index 99% rename from example/django_op/example/unical_accounts/models.py rename to example/django_op/example/accounts/models.py index 2fa08c1f..bc994780 100644 --- a/example/django_op/example/unical_accounts/models.py +++ b/example/django_op/example/accounts/models.py @@ -26,7 +26,7 @@ class User(AbstractUser): 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=30, + 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', diff --git a/example/django_op/example/unical_accounts/templatetags/__init__.py b/example/django_op/example/accounts/templatetags/__init__.py similarity index 100% rename from example/django_op/example/unical_accounts/templatetags/__init__.py rename to example/django_op/example/accounts/templatetags/__init__.py diff --git a/example/django_op/example/unical_accounts/templatetags/has_group.py b/example/django_op/example/accounts/templatetags/has_group.py similarity index 100% rename from example/django_op/example/unical_accounts/templatetags/has_group.py rename to example/django_op/example/accounts/templatetags/has_group.py diff --git a/example/django_op/example/unical_accounts/tests.py b/example/django_op/example/accounts/tests.py similarity index 100% rename from example/django_op/example/unical_accounts/tests.py rename to example/django_op/example/accounts/tests.py diff --git a/example/django_op/example/unical_accounts/urls.py b/example/django_op/example/accounts/urls.py similarity index 96% rename from example/django_op/example/unical_accounts/urls.py rename to example/django_op/example/accounts/urls.py index 7a56fbd5..7bd77a54 100644 --- a/example/django_op/example/unical_accounts/urls.py +++ b/example/django_op/example/accounts/urls.py @@ -16,11 +16,11 @@ from django.urls import path from .views import * -app_name="unical_accounts" +app_name="accounts" urlpatterns = [ # url(r'^login/$', Login, name='login'), # path('logout', Logout, name='logout'), - + ] diff --git a/example/django_op/example/unical_accounts/views.py b/example/django_op/example/accounts/views.py similarity index 100% rename from example/django_op/example/unical_accounts/views.py rename to example/django_op/example/accounts/views.py diff --git a/example/django_op/example/data/oidc_rp/conf.django.yaml b/example/django_op/example/data/oidc_rp/conf.django.yaml index f5242b4b..699a6f89 100644 --- a/example/django_op/example/data/oidc_rp/conf.django.yaml +++ b/example/django_op/example/data/oidc_rp/conf.django.yaml @@ -1,15 +1,42 @@ -PORT: 8099 -BASEURL: "https://127.0.0.1:8099" +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 -SERVER_CERT: "certs/cert.pem" -SERVER_KEY: "certs/key.pem" -CA_BUNDLE: '' +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 -VERIFY_SSL: false +httpc_params: + verify: False -KEYDEFS: &keydef +key_defs: &keydef - "type": "RSA" "key": '' @@ -19,60 +46,85 @@ KEYDEFS: &keydef "crv": "P-256" "use": ["sig"] -HTML_HOME: 'html' -SECRET_KEY: 'secret_key' -SESSION_COOKIE_NAME: 'rp_session' -PREFERRED_URL_SCHEME: 'https' +# html_home: 'html' +# secret_key: 'secret_key' +# session_cookie_name: 'rp_session' +# preferred_url_scheme: 'https' -RP_KEYS: +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 -# PUBLIC_JWKS_PATH: 'https://127.0.0.1:8090/static/jwks.json' -# PRIVATE_JWKS_PATH: './private/jwks.json' - -client_preferences: &id001 - application_name: rphandler +# 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, profile, email, address, phone] - token_endpoint_auth_method: [client_secret_basic, client_secret_post] + 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: &id002 - discovery: +services: &services + discovery: &disc class: oidcservice.oidc.provider_info_discovery.ProviderInfoDiscovery kwargs: {} - registration: + registration: ®ist class: oidcservice.oidc.registration.Registration kwargs: {} - authorization: + authorization: &authz class: oidcservice.oidc.authorization.Authorization kwargs: {} - accesstoken: + accesstoken: &acctok class: oidcservice.oidc.access_token.AccessToken kwargs: {} - refresh_accesstoken: - class: oidcservice.oidc.refresh_access_token.RefreshAccessToken - kwargs: {} - userinfo: + userinfo: &userinfo class: oidcservice.oidc.userinfo.UserInfo kwargs: {} - end_session: + 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 -CLIENTS: django_oidc_op: - client_preferences: *id001 + 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 - redirect_uris: ['https://127.0.0.1:8099/authz_cb/django_oidc_op'] - services: *id002 + + 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 @@ -80,5 +132,28 @@ CLIENTS: code_challenge_length: 64 code_challenge_method: S256 -# Whether an attempt to fetch the userinfo should be made -USERINFO: true + 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 new file mode 100644 index 00000000..e12541e2 --- /dev/null +++ b/example/django_op/example/data/oidc_rp/conf.json @@ -0,0 +1,322 @@ +{ + "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/oidc_op.conf.yaml b/example/django_op/example/example/oidc_op.conf.yaml index 2a88e64e..d6738699 100644 --- a/example/django_op/example/example/oidc_op.conf.yaml +++ b/example/django_op/example/example/oidc_op.conf.yaml @@ -1,9 +1,7 @@ # TODO: the following four should be handled in settings.py -session_cookie_name: django_oidc_op -port: &port 8000 -domain: &domain 127.0.0.1 -server_name: &server_name 127.0.0.1:8000 -base_url: &base_url https://127.0.0.1:8000 +# 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 - @@ -24,26 +22,22 @@ OIDC_KEYS: &oidc_keys # otherwise OP metadata will have jwks_uri: https://127.0.0.1:5000/None! 'uri_path': 'static/jwks.json' -session_jwk: 'data/oidc_op/private/session.json' - op: server_info: issuer: *base_url - verify_ssl: False + httpc_params: + verify: False + seed: asdasdasdasd-random + session_key: + filename: data/oidc_op/private/session_jwks.json + type: OCT + use: sig capabilities: - 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 + # 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 @@ -52,26 +46,12 @@ op: - implicit - urn:ietf:params:oauth:grant-type:jwt-bearer - refresh_token - # TODO: Django mapping of claims -> user attrs - claim_types_supported: - - normal - - aggregated - - distributed - # filter as you prefer, see oidcmsg(.__init__.)SCOPE2CLAIMS - # defaults are ['family_name', 'email_verified', 'profile', 'given_name', 'picture', 'address', 'name', 'sub', 'email', 'zoneinfo', 'gender', 'website', 'phone_number_verified', 'middle_name', 'nickname', 'birthdate', 'preferred_username', 'locale', 'updated_at', 'phone_number'] - # claims_supported: - # - picture - # - nickname - claims_parameter_supported: True - request_parameter_supported: True - request_uri_parameter_supported: True - frontchannel_logout_supported: True - frontchannel_logout_session_supported: True - backchannel_logout_supported: True - backchannel_logout_session_supported: True - check_session_iframe: https://127.0.0.1:5000/check_session_iframe + 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: oidcop.id_token.IDToken + class: oidcendpoint.id_token.IDToken kwargs: default_claims: email: @@ -80,7 +60,7 @@ op: essential: True token_handler_args: jwks_def: - private_path: data/oidc_op/private/token_jwk.json + private_path: data/oidc_op/private/token_jwks.json read_only: False key_defs: - @@ -98,7 +78,7 @@ op: code: lifetime: 600 token: - class: oidcop.jwt_token.JWTToken + class: oidcendpoint.token.jwt_token.JWTToken lifetime: 3600 add_claims: - email @@ -107,10 +87,10 @@ op: - phone_number_verified add_claim_by_scope: True aud: - - https://127.0.0.1:8000 + - *base_url refresh: lifetime: 86400 - jwks: + keys: *oidc_keys template_dir: oidc_op/templates @@ -119,32 +99,34 @@ op: endpoint: webfinger: path: '.well-known/webfinger' - class: oidcop.oidc.discovery.Discovery + class: oidcendpoint.oidc.discovery.Discovery kwargs: # TODO: optionally manage discovery service authn client_authn_method: null provider_info: path: ".well-known/openid-configuration" - class: oidcop.oidc.provider_config.ProviderConfiguration + class: oidcendpoint.oidc.provider_config.ProviderConfiguration kwargs: # TODO: optionally manage openid-configuration authn client_authn_method: null registration: path: registration - class: oidcop.oidc.registration.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: oidcop.oidc.read_registration.RegistrationRead + class: oidcendpoint.oidc.read_registration.RegistrationRead kwargs: client_authn_method: - bearer_header introspection: path: introspection - class: oidcop.oauth2.introspection.Introspection + class: oidcendpoint.oauth2.introspection.Introspection kwargs: client_authn_method: client_secret_post: ClientSecretPost @@ -152,14 +134,30 @@ op: - username authorization: path: authorization - class: oidcop.oidc.authorization.Authorization + class: oidcendpoint.oidc.authorization.Authorization kwargs: - # TODO: make a authn method for 'client_authn_method' client_authn_method: null + claims_parameter_supported: True + request_parameter_supported: True + request_uri_parameter_supported: True + response_types_supported: + - code + - token + - id_token + - "code token" + - "code id_token" + - "id_token token" + - "code id_token token" + - none + response_modes_supported: + - query + - fragment + - form_post token: path: token - class: oidcop.oidc.token.AccessToken + class: oidcendpoint.oidc.token.Token kwargs: + allow_refresh: False client_authn_method: - client_secret_post - client_secret_basic @@ -167,14 +165,24 @@ op: - private_key_jwt userinfo: path: userinfo - class: oidcop.oidc.userinfo.UserInfo + class: oidcendpoint.oidc.userinfo.UserInfo + kwargs: + claim_types_supported: + - normal + - aggregated + - distributed end_session: path: session - class: oidcop.oidc.session.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: @@ -190,7 +198,7 @@ op: updated_at: get_oidc_lastlogin authentication: user: - acr: oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD + acr: oidcendpoint.user_authn.authn_context.INTERNETPROTOCOLPASSWORD class: oidc_op.users.UserPassDjango kwargs: # this would override web resource where credentials will be POSTed @@ -199,7 +207,7 @@ op: template: oidc_login.html # args1: - # class: oidcop.util.JSONDictDB + # class: oidcendpoint.util.JSONDictDB # kwargs: # args1_1: data/oidc_op/things.json @@ -208,35 +216,48 @@ op: user_label: "Nickname" passwd_label: "Secret sauce" #anon: - #acr: oidcop.user_authn.authn_context.UNSPECIFIED - #class: oidcop.user_authn.user.NoAuthn + #acr: oidcendpoint.user_authn.authn_context.UNSPECIFIED + #class: oidcendpoint.user_authn.user.NoAuthn #kwargs: #user: thatusername - cookie_dealer: - class: oidcop.cookie.CookieDealer - kwargs: - sign_jwk: data/oidc_op/private/cookie_sign_jwk.json - sign_alg: 'SHA256' + # 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' + # ## 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 - default_values: - name: oidc_op - domain: *domain - path: / - max_age: 3600 login_hint2acrs: - class: oidcop.login_hint.LoginHint2Acrs + class: oidcendpoint.login_hint.LoginHint2Acrs kwargs: scheme_map: email: - - oidcop.user_authn.authn_context.INTERNETPROTOCOLPASSWORD + - oidcendpoint.user_authn.authn_context.INTERNETPROTOCOLPASSWORD - # this adds PKCE support as mandatory - disable if needed + # this adds PKCE support as mandatory - disable it if needed (essential: False) add_on: pkce: - function: oidcop.oidc.add_on.pkce.add_pkce_support + function: oidcendpoint.oidc.add_on.pkce.add_pkce_support kwargs: essential: True code_challenge_method: @@ -244,3 +265,65 @@ op: 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 new file mode 100644 index 00000000..c4872f2c --- /dev/null +++ b/example/django_op/example/example/oidc_provider_settings.py @@ -0,0 +1,367 @@ +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 + }, + "id_token": { + "class": "oidcop.id_token.IDToken", + "kwargs": { + "default_claims": { + "email": { + "essential": True + }, + "email_verified": { + "essential": True + } + } + } + }, + "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 index 980b5f59..828b452b 100644 --- a/example/django_op/example/example/settings.py +++ b/example/django_op/example/example/settings.py @@ -31,7 +31,7 @@ # Application definition INSTALLED_APPS = [ - 'unical_accounts', + 'accounts', 'django.contrib.admin', 'django.contrib.auth', @@ -40,10 +40,15 @@ 'django.contrib.messages', 'django.contrib.staticfiles', - 'oidc_op', + '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', @@ -85,7 +90,7 @@ } } -AUTH_USER_MODEL = "unical_accounts.User" +AUTH_USER_MODEL = "accounts.User" # Password validation # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators @@ -109,13 +114,9 @@ # 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 @@ -125,14 +126,12 @@ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'data/static') -OIDCENDPOINT_CONFIG = 'example/oidc_op.conf.yaml' - # LOGGING LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { - 'default': { + 'verbose': { # exact format is not important, this is the minimum information 'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', }, @@ -153,30 +152,59 @@ }, 'console': { 'formatter': 'detailed', - 'level': 'INFO', + 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { + 'django_test': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, 'django': { 'handlers': ['console', 'mail_admins'], 'level': 'ERROR', - 'propagate': False, + 'propagate': True, }, - 'oidc_op': { - 'handlers': ['console', 'mail_admins'], - 'level': 'DEBUG', - 'propagate': False, + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, }, - 'oidcendpoint': { + # 'django.server': { + # 'handlers': ['console'], + # 'level': 'INFO', + # 'propagate': False, + # }, + 'oidc_provider': { 'handlers': ['console', 'mail_admins'], 'level': 'DEBUG', - 'propagate': False, + 'propagate': True, }, - 'oidcmsg': { + '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 index 1d9a86ed..69c9bef1 100644 --- a/example/django_op/example/example/urls.py +++ b/example/django_op/example/example/urls.py @@ -25,6 +25,6 @@ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -if 'oidc_op' in settings.INSTALLED_APPS: - import oidc_op.urls - urlpatterns += path('', include((oidc_op.urls, 'oidc_op',))), +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/oidc_op b/example/django_op/example/oidc_op deleted file mode 120000 index 108427ed..00000000 --- a/example/django_op/example/oidc_op +++ /dev/null @@ -1 +0,0 @@ -../oidc_op/ \ No newline at end of file diff --git a/example/django_op/example/oidc_provider b/example/django_op/example/oidc_provider new file mode 120000 index 00000000..6e773bec --- /dev/null +++ b/example/django_op/example/oidc_provider @@ -0,0 +1 @@ +../oidc_provider \ No newline at end of file diff --git a/example/django_op/example/run.bash b/example/django_op/example/run.bash new file mode 100644 index 00000000..113f6617 --- /dev/null +++ b/example/django_op/example/run.bash @@ -0,0 +1,7 @@ +#!/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/example/unical_accounts/__init__.py b/example/django_op/example/unical_accounts/__init__.py deleted file mode 100644 index a4956ea5..00000000 --- a/example/django_op/example/unical_accounts/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -default_app_config = 'unical_accounts.apps.Unical_AccountsConfig' - diff --git a/example/django_op/oidc-op.dev.notes.md b/example/django_op/oidc-op.dev.notes.md new file mode 100644 index 00000000..83dbc9d3 --- /dev/null +++ b/example/django_op/oidc-op.dev.notes.md @@ -0,0 +1,260 @@ +# 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_op/__init__.py b/example/django_op/oidc_op/__init__.py deleted file mode 100644 index 154a4637..00000000 --- a/example/django_op/oidc_op/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . application import oidcendpoint_application - - -oidcendpoint_app = oidcendpoint_application() diff --git a/example/django_op/oidc_op/admin.py b/example/django_op/oidc_op/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/example/django_op/oidc_op/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/example/django_op/oidc_op/application.py b/example/django_op/oidc_op/application.py deleted file mode 100644 index 8b7183d5..00000000 --- a/example/django_op/oidc_op/application.py +++ /dev/null @@ -1,41 +0,0 @@ -import os - -from cryptojwt.key_jar import init_key_jar -from django.conf import settings -from oidcendpoint.endpoint_context import EndpointContext -from urllib.parse import urlparse - -from . configure import Configuration - - -def init_oidc_op_endpoints(app): - _config = app.srv_config.op - _server_info_config = _config['server_info'] - - _kj_args = {k:v for k,v in _server_info_config['jwks'].items() - if k != 'uri_path'} - _kj = init_key_jar(**_kj_args) - iss = _server_info_config['issuer'] - - # make sure I have a set of keys under my 'real' name - _kj.import_jwks_as_json(_kj.export_jwks_as_json(True, ''), iss) - _kj.verify_ssl = _config['server_info'].get('verify_ssl', False) - - endpoint_context = EndpointContext(_server_info_config, keyjar=_kj, - cwd=settings.BASE_DIR) - - return endpoint_context - - -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 oidcendpoint_application(config_file=settings.OIDCENDPOINT_CONFIG): - config = Configuration.create_from_config_file(config_file) - app = oidc_provider_init_app(config) - return app diff --git a/example/django_op/oidc_op/apps.py b/example/django_op/oidc_op/apps.py deleted file mode 100644 index 28699aa0..00000000 --- a/example/django_op/oidc_op/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class OidcOpConfig(AppConfig): - name = 'oidc_op' diff --git a/example/django_op/oidc_op/configure.py b/example/django_op/oidc_op/configure.py deleted file mode 100644 index 1b78fdd4..00000000 --- a/example/django_op/oidc_op/configure.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Configuration management for IDP""" - -import json -import os -import sys -from typing import Dict - -from cryptojwt.jwk.hmac import SYMKey -from cryptojwt.jwx import key_from_jwk_dict -from django.conf import settings - -from oidcop.logging import configure_logging -from oidcop.utils import load_yaml_config - -# TODO: check this -try: - from secrets import token_urlsafe as rnd_token -except ImportError: - from oidcop import rndstr as rnd_token - - -class Configuration: - """OP Configuration""" - - def __init__(self, conf: Dict) -> None: - self.logger = configure_logging(settings.LOGGING) - - # OIDC provider configuration - self.conf = conf - self.op = conf.get('op') - - # TODO: here manage static clients without dyn registration enabled - # self.oidc_clients = conf.get('oidc_clients', {}) - - # session key - self.session_jwk = conf.get('session_jwk') - if self.session_jwk is not None: - self.logger.debug("Reading session signer from %s", self.session_jwk) - try: - with open(self.session_jwk) as jwk_file: - jwk_dict = json.loads(jwk_file.read()) - self.session_key = key_from_jwk_dict(jwk_dict).k - except Exception: - self.logger.critical("Failed reading session signer from %s", - self.session_jwk) - sys.exit(-1) - else: - self.logger.debug("Generating random session signer") - self.session_key = SYMKey(key=rnd_token(32)).k - - # set OP session key - if self.op is not None: - if self.op['server_info'].get('password') is None: - key = self.session_key - self.op['server_info']['password'] = key - self.logger.debug("Set server password to %s", key) - - # templates environment - self.template_dir = os.path.abspath(conf.get('template_dir', 'templates')) - - - @classmethod - def create_from_config_file(cls, filename: str): - """Load configuration as YAML""" - return cls(load_yaml_config(filename)) diff --git a/example/django_op/oidc_op/models.py b/example/django_op/oidc_op/models.py deleted file mode 100644 index 71a83623..00000000 --- a/example/django_op/oidc_op/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/example/django_op/oidc_op/tests.py b/example/django_op/oidc_op/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/example/django_op/oidc_op/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/example/django_op/oidc_op/urls.py b/example/django_op/oidc_op/urls.py deleted file mode 100644 index 04a01de1..00000000 --- a/example/django_op/oidc_op/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.urls import path - -from . import views - -app_name = 'oidc_op' - -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('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_op/views.py b/example/django_op/oidc_op/views.py deleted file mode 100644 index c86c6cec..00000000 --- a/example/django_op/oidc_op/views.py +++ /dev/null @@ -1,289 +0,0 @@ -import base64 -import logging -import json -import os - -from django.conf import settings -from django.http import (HttpResponse, - HttpResponseBadRequest, - HttpResponseRedirect, - JsonResponse) -from django.views.decorators.csrf import csrf_exempt -from django.shortcuts import render, render_to_response -from django.urls import reverse -from django.utils.translation import gettext as _ -from oidcendpoint.authn_event import create_authn_event -from oidcendpoint.exception import FailedAuthentication -from oidcendpoint.oidc.token import AccessToken -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 oidc_op import oidcendpoint_app - - -logger = logging.getLogger(__name__) - - -def _add_cookie(resp, cookie_spec): - for key, _morsel in cookie_spec.items(): - kwargs = {'value': _morsel.value} - for param in ['expires', 'path', 'comment', 'domain', 'max-age', - 'secure', - 'version']: - if _morsel[param]: - kwargs[param] = _morsel[param] - resp.set_cookie(key, **kwargs) - - -def add_cookie(resp, cookie_spec): - if isinstance(cookie_spec, list): - for _spec in cookie_spec: - _add_cookie(resp, _spec) - - -def do_response(endpoint, req_args, error='', **args): - info = endpoint.do_response(request=req_args, error=error, **args) - - logger = oidcendpoint_app.srv_config.logger - logger.debug('do_response: {}'.format(info)) - - try: - _response_placement = info['response_placement'] - except KeyError: - _response_placement = endpoint.response_placement - - logger.debug('response_placement: {}'.format(_response_placement)) - - if error: - if _response_placement == 'body': - logger.info('Error Response: {}'.format(info['response'])) - resp = HttpResponse(info['response'], status=400) - else: # _response_placement == 'url': - logger.info('Redirect to: {}'.format(info['response'])) - resp = HttpResponseRedirect(info['response']) - else: - if _response_placement == 'body': - logger.info('Response: {}'.format(info['response'])) - resp = HttpResponse(info['response'], status=200) - else: # _response_placement == 'url': - logger.info('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 service_endpoint(request, endpoint): - """ - TODO: documentation here - """ - logger = oidcendpoint_app.srv_config.logger - logger.info('At the "{}" endpoint'.format(endpoint.endpoint_name)) - - # if hasattr(request, 'debug') and request.debug: - # import pdb; pdb.set_trace() - - authn = request.headers.get('Authorization', {}) - pr_args = {'auth': authn} - if authn: - logger.debug('request.headers["Authorization"] => {}'.format(pr_args)) - - 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 authn: - 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()} - - # for .well-known resources like provider-config no data are submitted - # if not data: - # ... not possible in this implementation - - logger.debug('Request arguments [{}]: {}'.format(request.method, data)) - try: - req_args = endpoint.parse_request(data, **pr_args) - except Exception as err: - logger.error(err) - return JsonResponse(json.dumps({ - 'error': 'invalid_request', - 'error_description': str(err), - 'method': request.method - }), status=400) - - logger.info('request: {}'.format(req_args)) - if isinstance(req_args, ResponseMessage) and 'error' in req_args: - return JsonResponse(req_args.__dict__, status=400) - - if request.COOKIES: - logger.debug(request.COOKIES) - # TODO: cookie - kwargs = {'cookie': request.COOKIES} - else: - kwargs = {} - - try: - if isinstance(endpoint, AccessToken): - args = endpoint.process_request(AccessTokenRequest(**req_args), - **kwargs) - else: - args = endpoint.process_request(req_args, **kwargs) - except Exception as err: - message = '{}'.format(err) - logger.error(message) - return JsonResponse(json.dumps({ - 'error': 'invalid_request', - 'error_description': str(err) - }), status=400) - - logger.info('Response args: {}'.format(args)) - 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 = oidcendpoint_app.endpoint_context.endpoint['provider_config'] - # TODO - # if service == 'openid-federation': - # _endpoint = oidcendpoint_app.endpoint_context.endpoint['provider_info'] - elif service == 'webfinger': - _endpoint = oidcendpoint_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 = oidcendpoint_app.endpoint_context.endpoint['registration'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def registration_api(): - logger.info('registration API') - return service_endpoint(request, - oidcendpoint_app.endpoint_context.endpoint['registration_api']) - - -def authorization(request): - _endpoint = oidcendpoint_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 = oidcendpoint_app.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 = oidcendpoint_app.endpoint_context.endpoint['authorization'] - args = endpoint.authz_part2(user=user.username, request=authz_request, - authn_event=authn_event) - - 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 = oidcendpoint_app.endpoint_context.endpoint['token'] - return service_endpoint(request, _endpoint) - - -@csrf_exempt -def userinfo(request): - logger.info('userinfo request') - _endpoint = oidcendpoint_app.endpoint_context.endpoint['userinfo'] - # if not hasattr(request, 'debug'): - # request.debug = 0 - # request.debug +=1 - return service_endpoint(request, _endpoint) - - -######## -# LOGOUT -######## -def session_endpoint(request): - return service_endpoint(request, - oidcendpoint_app.endpoint_context.endpoint['session']) - -@csrf_exempt -def rp_logout(request): - _endp = oidcendpoint_app.endpoint_context.endpoint['session'] - _info = _endp.unpack_signed_jwt(request.POST['sjwt']) - alla = 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_to_response('frontchannel_logout.html', d) - - else: - res = HttpResponseRedirect(_info['redirect_uri']) - _kakor = _endp.kill_cookies() - _add_cookie(res, _kakor) - - return res - -def verify_logout(request): - part = urlparse(oidcendpoint_app.endpoint_context.issuer) - d = dict(op=part.hostname, - do_logout='rp_logout', - sjwt=request.GET['sjwt'] or request.POST['sjwt']) - return render_to_response('logout.html', d) - - -def post_logout(request): - return render_to_response('post_logout.html') diff --git a/example/django_op/oidc_provider/__init__.py b/example/django_op/oidc_provider/__init__.py new file mode 100644 index 00000000..4bd5dbb5 --- /dev/null +++ b/example/django_op/oidc_provider/__init__.py @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000..7a3c7456 --- /dev/null +++ b/example/django_op/oidc_provider/admin.py @@ -0,0 +1,214 @@ +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 new file mode 100644 index 00000000..7dba7f2a --- /dev/null +++ b/example/django_op/oidc_provider/application.py @@ -0,0 +1,49 @@ +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 new file mode 100644 index 00000000..a25c1655 --- /dev/null +++ b/example/django_op/oidc_provider/apps.py @@ -0,0 +1,6 @@ +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 new file mode 100644 index 00000000..c4a063df --- /dev/null +++ b/example/django_op/oidc_provider/migrations/0001_initial.py @@ -0,0 +1,155 @@ +# 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_op/migrations/__init__.py b/example/django_op/oidc_provider/migrations/__init__.py similarity index 100% rename from example/django_op/oidc_op/migrations/__init__.py rename to example/django_op/oidc_provider/migrations/__init__.py diff --git a/example/django_op/oidc_provider/models.py b/example/django_op/oidc_provider/models.py new file mode 100644 index 00000000..b8fb1aba --- /dev/null +++ b/example/django_op/oidc_provider/models.py @@ -0,0 +1,582 @@ +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/tests.py b/example/django_op/oidc_provider/tests.py new file mode 100644 index 00000000..49290204 --- /dev/null +++ b/example/django_op/oidc_provider/tests.py @@ -0,0 +1,2 @@ + +# 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 new file mode 100644 index 00000000..3653f310 --- /dev/null +++ b/example/django_op/oidc_provider/tests/01_client_db.py @@ -0,0 +1,102 @@ +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 new file mode 100644 index 00000000..e69de29b diff --git a/example/django_op/oidc_provider/urls.py b/example/django_op/oidc_provider/urls.py new file mode 100644 index 00000000..5d58e825 --- /dev/null +++ b/example/django_op/oidc_provider/urls.py @@ -0,0 +1,33 @@ +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_op/users.py b/example/django_op/oidc_provider/users.py similarity index 78% rename from example/django_op/oidc_op/users.py rename to example/django_op/oidc_provider/users.py index dbc4c331..65c584bb 100644 --- a/example/django_op/oidc_op/users.py +++ b/example/django_op/oidc_provider/users.py @@ -4,32 +4,29 @@ from django.contrib.auth import get_user_model from django.template.loader import render_to_string -from oidcendpoint.util import instantiate -from oidcendpoint.user_authn.user import (create_signed_jwt, - verify_signed_jwt) -from oidcendpoint.user_authn.user import UserAuthnMethod +from oidcop.user_authn.user import (create_signed_jwt, LABELS) +from oidcop.user_authn.user import UserAuthnMethod class UserPassDjango(UserAuthnMethod): """ - see oidcendpoint.authn_context - oidcendpoint.endpoint_context + 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", - endpoint_context=None, verify_endpoint='', **kwargs): + 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__(endpoint_context=endpoint_context) + super(UserPassDjango, self).__init__(server_get=server_get) self.kwargs = kwargs self.kwargs.setdefault("page_header", "Log in") @@ -50,9 +47,8 @@ def __init__(self, self.action = verify_endpoint or self.url_endpoint self.kwargs['action'] = self.action - def __call__(self, **kwargs): - _ec = self.endpoint_context + _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) @@ -62,13 +58,9 @@ def __call__(self, **kwargs): _kwargs = self.kwargs.copy() for attr in ['policy', 'tos', 'logo']: _uri = '{}_uri'.format(attr) - try: - _kwargs[_uri] = kwargs[_uri] - except KeyError: - pass - else: - _label = '{}_label'.format(attr) - _kwargs[_label] = LABELS[_uri] + _kwargs[_uri] = kwargs.get(_uri) + _label = '{}_label'.format(attr) + _kwargs[_label] = LABELS[_uri] return self.template_handler(self.template, _kwargs) @@ -76,7 +68,8 @@ def verify(self, *args, **kwargs): username = kwargs["username"] password = kwargs["password"] - user = authenticate(username=username, password=password) + user = authenticate(username=username, + password=password) if username: return user @@ -112,18 +105,21 @@ def filter(self, user, user_info_claims=None): 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[key]) and restr == {"essential": True}: + 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 + 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): + def __call__(self, user_id, client_id, + user_info_claims=None, **kwargs): """ user_id = username client_id = client id, ex: 'mHwpZsDeWo5g' @@ -133,10 +129,7 @@ def __call__(self, user_id, client_id, user_info_claims=None, **kwargs): # Todo: raise exception here, this wouldn't be possible. return {} - try: - return self.filter(user, user_info_claims) - except KeyError: - return {} + return self.filter(user, user_info_claims) def search(self, **kwargs): for uid, args in self.db.items(): diff --git a/example/django_op/oidc_provider/utils.py b/example/django_op/oidc_provider/utils.py new file mode 100644 index 00000000..1700178d --- /dev/null +++ b/example/django_op/oidc_provider/utils.py @@ -0,0 +1,31 @@ +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 new file mode 100644 index 00000000..89b7f023 --- /dev/null +++ b/example/django_op/oidc_provider/views.py @@ -0,0 +1,351 @@ +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 index 64166d39..263f1c08 100644 --- a/example/django_op/requirements.txt +++ b/example/django_op/requirements.txt @@ -1,3 +1,10 @@ -Django>=2.2.4 +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 new file mode 100644 index 00000000..196acc72 --- /dev/null +++ b/example/django_op/snippets/db_interfaces.py @@ -0,0 +1,287 @@ +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': 'oidcendpoint.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 new file mode 100644 index 00000000..922e571d --- /dev/null +++ b/example/django_op/snippets/msg_test.py @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..27ae4e57 --- /dev/null +++ b/example/django_op/snippets/rp_handler.py @@ -0,0 +1,178 @@ +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/run.sh b/example/flask_op/run.sh new file mode 100644 index 00000000..ac121302 --- /dev/null +++ b/example/flask_op/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python3 server.py config.json