From db59882f33c99b3dbbbd8d938b548e0119fe9cd6 Mon Sep 17 00:00:00 2001 From: gabdevele Date: Wed, 17 Jun 2026 20:43:56 +0200 Subject: [PATCH] feat: link local library --- Dockerfile | 6 +- vendor/spid-cie-oidc-django/README.md | 233 ++++++++ vendor/spid-cie-oidc-django/setup.py | 75 +++ .../spid_cie_oidc/__init__.py | 1 + .../spid_cie_oidc/accounts/__init__.py | 0 .../spid_cie_oidc/accounts/admin.py | 74 +++ .../spid_cie_oidc/accounts/admin_inlines.py | 0 .../spid_cie_oidc/accounts/apps.py | 9 + .../accounts/migrations/0001_initial.py | 145 +++++ ...remove_user_taxpayer_id_user_attributes.py | 22 + .../migrations/0003_alter_user_attributes.py | 18 + .../accounts/migrations/__init__.py | 0 .../spid_cie_oidc/accounts/models.py | 35 ++ .../accounts/templatetags/__init__.py | 0 .../accounts/templatetags/has_group.py | 10 + .../spid_cie_oidc/authority/__init__.py | 0 .../spid_cie_oidc/authority/admin.py | 47 ++ .../spid_cie_oidc/authority/apps.py | 8 + .../authority/migrations/0001_initial.py | 310 ++++++++++ ...rationentityprofile_trust_mark_template.py | 20 + ...rationdescendantcontact_entity_and_more.py | 50 ++ ...0004_federationdescendant_jwks_and_more.py | 22 + .../0005_alter_federationdescendant_jwks.py | 19 + .../authority/migrations/0006_stafftoken.py | 32 + .../migrations/0007_stafftoken_expire_at.py | 18 + .../migrations/0008_alter_stafftoken_token.py | 19 + .../migrations/0009_delete_stafftoken.py | 16 + ..._federationdescendant_metadata_and_more.py | 30 + ...ederationdescendant_issue_x509_and_more.py | 54 ++ ..._remove_federationdescendant_issue_x509.py | 16 + ...lter_federationdescendant_type_and_more.py | 48 ++ ...lter_federationdescendant_type_and_more.py | 23 + .../authority/migrations/__init__.py | 0 .../spid_cie_oidc/authority/models.py | 353 +++++++++++ .../authority/schemas/__init__.py | 0 .../schemas/advanced_entity_list_endpoint.py | 39 ++ .../schemas/fetch_endpoint_request.py | 34 ++ .../authority/schemas/list_endpoint.py | 20 + .../schemas/trust_mark_status_endpoint.py | 35 ++ .../spid_cie_oidc/authority/settings.py | 59 ++ .../spid_cie_oidc/authority/tests/__init__.py | 0 .../authority/tests/mocked_responses.py | 165 ++++++ .../spid_cie_oidc/authority/tests/settings.py | 149 +++++ .../test_02_trust_anchor_intermediary.py | 385 ++++++++++++ .../authority/tests/test_05_validators.py | 48 ++ .../tests/test_06_advanced_entity_listing.py | 49 ++ .../authority/tests/test_08_schemas.py | 51 ++ .../spid_cie_oidc/authority/urls.py | 42 ++ .../spid_cie_oidc/authority/utils.py | 5 + .../spid_cie_oidc/authority/validators.py | 60 ++ .../spid_cie_oidc/authority/views.py | 301 ++++++++++ .../spid_cie_oidc/entity/__init__.py | 6 + .../spid_cie_oidc/entity/abstract_models.py | 30 + .../spid_cie_oidc/entity/admin.py | 202 +++++++ .../spid_cie_oidc/entity/apps.py | 8 + .../spid_cie_oidc/entity/exceptions.py | 53 ++ .../spid_cie_oidc/entity/http_client.py | 44 ++ .../spid_cie_oidc/entity/jwk_x5c.py | 86 +++ .../spid_cie_oidc/entity/jwks.py | 121 ++++ .../spid_cie_oidc/entity/jwtse.py | 137 +++++ .../entity/migrations/0001_initial.py | 243 ++++++++ .../migrations/0002_fetchedentitystatement.py | 53 ++ ...derationentityconfiguration_constraints.py | 21 + ...yconfiguration_authority_hints_and_more.py | 30 + ...derationentityconfiguration_constraints.py | 25 + ..._trustchain_resultant_metadata_and_more.py | 56 ++ ...ter_trustchain_unique_together_and_more.py | 37 ++ .../0008_fetchedentitystatement_jwt.py | 19 + .../0009_trustchain_trust_marks_and_more.py | 29 + ...derationentityconfiguration_entity_type.py | 29 + .../0011_alter_trustchain_status.py | 30 + .../migrations/0012_delete_publicjwk.py | 16 + ..._federationentityconfiguration_metadata.py | 19 + ...ter_trustchain_unique_together_and_more.py | 21 + ...nique_together_trustchain_type_and_more.py | 26 + ...ter_trustchain_unique_together_and_more.py | 21 + ...rationentityconfiguration_jwks_and_more.py | 28 + .../entity/migrations/0018_trustchain_jwks.py | 19 + .../entity/migrations/0019_stafftoken.py | 33 ++ ...nentityconfiguration_jwks_core_and_more.py | 33 ++ .../0021_federationhistoricalkey.py | 86 +++ ..._0026_alter_federationhistoricalkey_jwk.py | 79 +++ ...22_federationhistoricalkey_jwk_and_more.py | 23 + .../0023_alter_federationhistoricalkey_jwk.py | 17 + .../0024_alter_federationhistoricalkey_kid.py | 17 + .../0025_alter_federationhistoricalkey_jwk.py | 17 + .../0026_alter_federationhistoricalkey_jwk.py | 17 + ...tionhistoricalkey_revocation_motivation.py | 35 ++ ...ederationentityconfiguration_issue_x509.py | 23 + ...ntityconfiguration_entity_type_and_more.py | 38 ++ ...ederationentityconfiguration_issue_x509.py | 19 + ...derationentityconfiguration_entity_type.py | 34 ++ .../0032_alter_fetchedentitystatement_jwt.py | 20 + ...derationentityconfiguration_entity_type.py | 18 + .../entity/migrations/__init__.py | 0 .../spid_cie_oidc/entity/models.py | 550 ++++++++++++++++++ .../spid_cie_oidc/entity/policy.py | 355 +++++++++++ .../spid_cie_oidc/entity/schemas/__init__.py | 0 .../entity/schemas/fa_metadata.py | 12 + .../spid_cie_oidc/entity/schemas/jwks.py | 72 +++ .../entity/schemas/op_metadata.py | 138 +++++ .../entity/schemas/resolve_endpoint.py | 53 ++ .../entity/schemas/rp_metadata.py | 46 ++ .../spid_cie_oidc/entity/settings.py | 93 +++ .../spid_cie_oidc/entity/statements.py | 486 ++++++++++++++++ .../spid_cie_oidc/entity/tests/__init__.py | 8 + .../entity/tests/fa_metadata_settings.py | 9 + .../entity/tests/jwks_settings.py | 33 ++ .../entity/tests/op_metadata_settings.py | 91 +++ .../entity/tests/rp_metadata_settings.py | 97 +++ .../spid_cie_oidc/entity/tests/settings.py | 44 ++ .../tests/test_01_entity_configuration.py | 55 ++ .../entity/tests/test_01_op_metadata.py | 37 ++ .../entity/tests/test_02_rp_metadata.py | 43 ++ .../entity/tests/test_03_jwks.py | 24 + .../entity/tests/test_04_fa_metadata.py | 8 + .../entity/tests/test_05_policy.py | 91 +++ .../entity/tests/test_06_http_client.py | 12 + .../tests/test_07_resolve_entity_statement.py | 96 +++ .../entity/tests/test_07_validators.py | 81 +++ .../entity/tests/test_08_statements.py | 40 ++ .../entity/tests/test_09_trust_chain.py | 59 ++ .../entity/tests/test_10_schemas.py | 21 + .../spid_cie_oidc/entity/trust_chain.py | 317 ++++++++++ .../entity/trust_chain_operations.py | 203 +++++++ .../spid_cie_oidc/entity/urls.py | 39 ++ .../spid_cie_oidc/entity/utils.py | 79 +++ .../spid_cie_oidc/entity/validators.py | 104 ++++ .../spid_cie_oidc/entity/views.py | 257 ++++++++ .../spid_cie_oidc/entity/x509.py | 101 ++++ .../spid_cie_oidc/onboarding/__init__.py | 0 .../spid_cie_oidc/onboarding/admin.py | 47 ++ .../spid_cie_oidc/onboarding/apps.py | 8 + .../spid_cie_oidc/onboarding/forms.py | 134 +++++ .../onboarding/migrations/0001_initial.py | 85 +++ ...onboardingregistration_options_and_more.py | 35 ++ ...03_alter_onboardingregistration_options.py | 24 + ...lter_onboardingregistration_public_jwks.py | 19 + ...005_alter_onboardingregistration_status.py | 18 + .../0006_onboardingregistration_type.py | 18 + .../0007_onboardingregistration_contact.py | 20 + ...onboardingregistration_auth_request_url.py | 18 + .../0009_alter_onboardingregistration_type.py | 30 + .../0010_alter_onboardingregistration_type.py | 31 + .../0011_alter_onboardingregistration_type.py | 18 + .../onboarding/migrations/__init__.py | 0 .../spid_cie_oidc/onboarding/models.py | 81 +++ .../static/images/SPID-e-CIE-696x383.jpg | Bin 0 -> 12269 bytes .../onboarding/static/images/logo-it.svg | 10 + .../onboarding/templates/base.html | 202 +++++++ .../templates/onboarding_apply_policy.html | 65 +++ .../templates/onboarding_convert_jwk.html | 47 ++ .../templates/onboarding_convert_pem.html | 51 ++ .../templates/onboarding_decode_jwt.html | 75 +++ .../templates/onboarding_entities.html | 80 +++ .../onboarding/templates/onboarding_jwk.html | 31 + .../templates/onboarding_landing.html | 82 +++ .../templates/onboarding_registration.html | 59 ++ .../onboarding_resolve_statement.html | 59 ++ .../templates/onboarding_schemas.html | 34 ++ .../templates/onboarding_validate_ec.html | 45 ++ .../templates/onboarding_validate_md.html | 33 ++ .../templates/onboarding_validating_tm.html | 41 ++ .../onboarding/tests/__init__.py | 0 .../onboarding/tests/mocked_responses.py | 17 + .../onboarding/tests/test_01_onboarding.py | 111 ++++ .../onboarding/tests/test_02_tools.py | 252 ++++++++ .../onboarding/tests/test_11_admin.py | 25 + .../onboarding/tests/tools_settings.py | 15 + .../spid_cie_oidc/onboarding/urls.py | 145 +++++ .../spid_cie_oidc/onboarding/validators.py | 12 + .../spid_cie_oidc/onboarding/views.py | 458 +++++++++++++++ .../spid_cie_oidc/provider/__init__.py | 0 .../spid_cie_oidc/provider/admin.py | 36 ++ .../spid_cie_oidc/provider/apps.py | 8 + .../spid_cie_oidc/provider/exceptions.py | 22 + .../spid_cie_oidc/provider/forms.py | 38 ++ .../provider/management/__init__.py | 0 .../provider/management/commands/__init__.py | 0 .../commands/fetch_openid_relying_parties.py | 106 ++++ .../provider/migrations/0001_initial.py | 84 +++ .../0002_remove_oidcsession_user_claims.py | 17 + ...ession_sub_issuedtoken_expires_and_more.py | 29 + .../migrations/0004_alter_oidcsession_user.py | 21 + .../migrations/0005_oidcsession_sid.py | 18 + .../migrations/0006_oidcsession_acr.py | 19 + ...token_options_alter_oidcsession_options.py | 21 + .../0008_alter_oidcsession_authz_request.py | 20 + .../provider/migrations/__init__.py | 0 .../spid_cie_oidc/provider/models.py | 107 ++++ .../spid_cie_oidc/provider/processors.py | 8 + .../provider/schemas/__init__.py | 0 .../provider/schemas/authn_requests.py | 222 +++++++ .../provider/schemas/authn_response.py | 44 ++ .../provider/schemas/client_assertion.py | 32 + .../provider/schemas/introspection_request.py | 11 + .../schemas/introspection_response.py | 34 ++ .../spid_cie_oidc/provider/schemas/jwt.py | 67 +++ .../provider/schemas/revocation_request.py | 12 + .../provider/schemas/revocation_response.py | 14 + .../provider/schemas/token_requests.py | 43 ++ .../provider/schemas/token_response.py | 19 + .../spid_cie_oidc/provider/settings.py | 207 +++++++ .../provider/static/css/provider.css | 352 +++++++++++ .../provider/static/images/logo-cie.png | Bin 0 -> 196297 bytes .../provider/static/svg/cie-ico-circle-bb.svg | 1 + .../provider/static/svg/logo-cie.svg | 39 ++ .../static/svg/no_image_available.svg | 14 + .../static/svg/spid-ico-circle-bb.svg | 1 + .../provider/static/svg/spid-logo-c-lb.svg | 1 + .../static/svg/spid-logo-c-lb.svg-LTS.svg | 117 ++++ .../provider/templates/op_base.html | 91 +++ .../provider/templates/op_user_consent.html | 109 ++++ .../provider/templates/op_user_history.html | 137 +++++ .../provider/templates/op_user_login.html | 118 ++++ .../provider/templatetags/__init__.py | 0 .../provider/templatetags/spid_cie_op.py | 24 + .../spid_cie_oidc/provider/tests/__init__.py | 0 .../provider/tests/authn_request_settings.py | 138 +++++ .../tests/authn_responses_settings.py | 52 ++ .../tests/introspection_request_settings.py | 32 + .../tests/introspection_response_settings.py | 69 +++ .../tests/revocation_request_settings.py | 32 + .../tests/revocation_response_settings.py | 20 + .../spid_cie_oidc/provider/tests/settings.py | 141 +++++ .../provider/tests/test_02_authn_endpoint.py | 353 +++++++++++ .../provider/tests/test_03_authn_request.py | 154 +++++ .../provider/tests/test_03_refresh_token.py | 191 ++++++ .../provider/tests/test_04_authn_response.py | 83 +++ .../tests/test_04_userinfo_endpoint.py | 111 ++++ .../tests/test_05_introspection_endpoint.py | 124 ++++ .../provider/tests/test_05_token_request.py | 147 +++++ .../provider/tests/test_06_processors.py | 11 + .../provider/tests/test_06_token_response.py | 77 +++ .../tests/test_07_fetch_relying_parties.py | 89 +++ .../tests/test_07_introspection_request.py | 53 ++ .../tests/test_08_introspection_response.py | 68 +++ .../provider/tests/test_08_token_endpoint.py | 145 +++++ .../tests/test_09_revocation_endpoint.py | 110 ++++ .../tests/test_09_revocation_request.py | 62 ++ .../provider/tests/test_10_no_consent.py | 18 + .../tests/test_10_revocation_response.py | 35 ++ .../tests/test_11_user_access_history.py | 70 +++ .../provider/tests/test_12_init.py | 36 ++ .../provider/tests/token_request_settings.py | 107 ++++ .../provider/tests/token_response_settings.py | 47 ++ .../spid_cie_oidc/provider/urls.py | 84 +++ .../spid_cie_oidc/provider/views/__init__.py | 414 +++++++++++++ .../provider/views/authz_request_view.py | 303 ++++++++++ .../spid_cie_oidc/provider/views/connect.py | 34 ++ .../provider/views/consent_page_view.py | 157 +++++ .../provider/views/introspection_endpoint.py | 97 +++ .../provider/views/revocation_endpoint.py | 105 ++++ .../provider/views/token_endpoint.py | 228 ++++++++ .../provider/views/userinfo_endpoint.py | 106 ++++ .../spid_cie_oidc/relying_party/__init__.py | 0 .../spid_cie_oidc/relying_party/admin.py | 78 +++ .../spid_cie_oidc/relying_party/apps.py | 8 + .../spid_cie_oidc/relying_party/exceptions.py | 2 + .../relying_party/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/fetch_openid_providers.py | 66 +++ .../relying_party/migrations/0001_initial.py | 90 +++ ...er_oidcauthentication_provider_and_more.py | 23 + ...idcauthenticationtoken_revoked_and_more.py | 23 + ...authenticationtoken_logged_out_and_more.py | 22 + ...idcauthenticationtoken_revoked_and_more.py | 28 + ...ication_provider_configuration_and_more.py | 24 + ..._alter_oidcauthentication_provider_jwks.py | 18 + ...ve_oidcauthentication_provider_and_more.py | 21 + ...0009_alter_oidcauthenticationtoken_code.py | 18 + .../relying_party/migrations/__init__.py | 0 .../spid_cie_oidc/relying_party/models.py | 80 +++ .../relying_party/oauth2/__init__.py | 73 +++ .../relying_party/oidc/__init__.py | 79 +++ .../spid_cie_oidc/relying_party/processors.py | 4 + .../spid_cie_oidc/relying_party/settings.py | 124 ++++ .../static/css/access-button.css | 413 +++++++++++++ .../static/js/spid-sp-access-button.js | 78 +++ .../relying_party/static/js/spid_button.js | 81 +++ .../static/svg/cie-ico-circle-bb-LTS.svg | 37 ++ .../static/svg/cie-ico-circle-bb.svg | 1 + .../relying_party/static/svg/favicon.ico | Bin 0 -> 7406 bytes .../static/svg/spid-ico-circle-bb.svg | 1 + .../templates/echo_attributes.html | 75 +++ .../relying_party/templates/rp_base.html | 92 +++ .../relying_party/templates/rp_error.html | 23 + .../templates/rp_introspection.html | 21 + .../relying_party/templates/rp_landing.html | 135 +++++ .../relying_party/tests/__init__.py | 0 .../relying_party/tests/mocked_response.py | 212 +++++++ .../relying_party/tests/test_01_rp_ops.py | 51 ++ .../tests/test_02_rp_commands.py | 15 + .../relying_party/tests/test_03_rp_begin.py | 198 +++++++ .../tests/test_04_rp_callback.py | 181 ++++++ .../tests/test_05_oidc_rpinitiated_logout.py | 77 +++ .../tests/test_06_fetch_openid_providers.py | 65 +++ .../tests/test_07_oidc_rp_landing.py | 40 ++ .../tests/test_08_callback_echo_attributes.py | 18 + .../relying_party/tests/test_09_rp_model.py | 62 ++ .../spid_cie_oidc/relying_party/urls.py | 78 +++ .../spid_cie_oidc/relying_party/utils.py | 75 +++ .../relying_party/views/__init__.py | 171 ++++++ .../relying_party/views/rp_begin.py | 214 +++++++ .../relying_party/views/rp_callback.py | 317 ++++++++++ .../views/rp_callback_echo_attributes.py | 19 + .../relying_party/views/rp_extend_session.py | 78 +++ .../views/rp_initiated_logout.py | 60 ++ .../relying_party/views/rp_introspection.py | 56 ++ .../relying_party/views/rp_landing.py | 60 ++ .../relying_party_test/__init__.py | 0 .../spid_cie_oidc/relying_party_test/admin.py | 28 + .../spid_cie_oidc/relying_party_test/apps.py | 8 + .../spid_cie_oidc/relying_party_test/forms.py | 28 + .../migrations/0001_initial.py | 55 ++ .../0002_alter_relyingpartytest_report.py | 19 + .../relying_party_test/migrations/__init__.py | 0 .../relying_party_test/models.py | 93 +++ .../relying_party_test/snippets/README.md | 23 + .../relying_party_test/snippets/crawler.py | 86 +++ .../snippets/pyppeteer_tests.py | 40 ++ .../static/images/oops.webp | Bin 0 -> 11982 bytes .../templates/op_user_staff_test.html | 219 +++++++ .../relying_party_test/tests/__init__.py | 0 .../tests/test_01_user_staff.py | 72 +++ .../spid_cie_oidc/relying_party_test/urls.py | 18 + .../spid_cie_oidc/relying_party_test/views.py | 73 +++ 327 files changed, 22274 insertions(+), 1 deletion(-) create mode 100644 vendor/spid-cie-oidc-django/README.md create mode 100644 vendor/spid-cie-oidc-django/setup.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin_inlines.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0002_remove_user_taxpayer_id_user_attributes.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0003_alter_user_attributes.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/has_group.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0002_alter_federationentityprofile_trust_mark_template.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0003_alter_federationdescendantcontact_entity_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0004_federationdescendant_jwks_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0005_alter_federationdescendant_jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0006_stafftoken.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0007_stafftoken_expire_at.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0008_alter_stafftoken_token.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0009_delete_stafftoken.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0010_federationdescendant_metadata_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0011_federationdescendant_issue_x509_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0012_remove_federationdescendant_issue_x509.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0013_alter_federationdescendant_type_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0014_alter_federationdescendant_type_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/advanced_entity_list_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/fetch_endpoint_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/list_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/trust_mark_status_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/mocked_responses.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_02_trust_anchor_intermediary.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_05_validators.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_06_advanced_entity_listing.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_08_schemas.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/utils.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/validators.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/authority/views.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/abstract_models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/exceptions.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/http_client.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwk_x5c.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwtse.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0002_fetchedentitystatement.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0003_federationentityconfiguration_constraints.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0004_alter_federationentityconfiguration_authority_hints_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0005_alter_federationentityconfiguration_constraints.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0006_remove_trustchain_resultant_metadata_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0007_alter_trustchain_unique_together_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0008_fetchedentitystatement_jwt.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0009_trustchain_trust_marks_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0010_federationentityconfiguration_entity_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0011_alter_trustchain_status.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0012_delete_publicjwk.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0013_alter_federationentityconfiguration_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0014_alter_trustchain_unique_together_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0015_alter_trustchain_unique_together_trustchain_type_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0016_alter_trustchain_unique_together_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0017_remove_federationentityconfiguration_jwks_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0018_trustchain_jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0019_stafftoken.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0020_alter_federationentityconfiguration_jwks_core_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0022_federationhistoricalkey_jwk_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0023_alter_federationhistoricalkey_jwk.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0024_alter_federationhistoricalkey_kid.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0025_alter_federationhistoricalkey_jwk.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0026_alter_federationhistoricalkey_jwk.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0027_alter_federationhistoricalkey_revocation_motivation.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0028_federationentityconfiguration_issue_x509.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0029_alter_federationentityconfiguration_entity_type_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0030_remove_federationentityconfiguration_issue_x509.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0031_alter_federationentityconfiguration_entity_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0033_alter_federationentityconfiguration_entity_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/policy.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/fa_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/op_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/resolve_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/rp_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/statements.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/fa_metadata_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/jwks_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/op_metadata_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/rp_metadata_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_entity_configuration.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_op_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_02_rp_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_03_jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_04_fa_metadata.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_05_policy.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_06_http_client.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_resolve_entity_statement.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_validators.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_08_statements.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_09_trust_chain.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_10_schemas.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain_operations.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/utils.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/validators.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/views.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/entity/x509.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/forms.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0002_alter_onboardingregistration_options_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0003_alter_onboardingregistration_options.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0004_alter_onboardingregistration_public_jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0005_alter_onboardingregistration_status.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0006_onboardingregistration_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0007_onboardingregistration_contact.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0008_onboardingregistration_auth_request_url.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0009_alter_onboardingregistration_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0010_alter_onboardingregistration_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0011_alter_onboardingregistration_type.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/SPID-e-CIE-696x383.jpg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/logo-it.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/base.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_apply_policy.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_jwk.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_pem.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_decode_jwt.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_entities.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_jwk.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_landing.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_registration.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_resolve_statement.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_schemas.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_ec.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_md.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validating_tm.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/mocked_responses.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_01_onboarding.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_02_tools.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_11_admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/tools_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/validators.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/views.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/exceptions.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/forms.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/fetch_openid_relying_parties.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0002_remove_oidcsession_user_claims.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0003_remove_oidcsession_sub_issuedtoken_expires_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0004_alter_oidcsession_user.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0005_oidcsession_sid.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0006_oidcsession_acr.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0007_alter_issuedtoken_options_alter_oidcsession_options.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0008_alter_oidcsession_authz_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/processors.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_requests.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/client_assertion.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/jwt.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_requests.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/css/provider.css create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/images/logo-cie.png create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/cie-ico-circle-bb.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/logo-cie.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/no_image_available.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-ico-circle-bb.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg-LTS.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_base.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_consent.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_history.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_login.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/spid_cie_op.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_request_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_responses_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_request_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_response_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_request_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_response_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_02_authn_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_authn_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_refresh_token.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_authn_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_userinfo_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_introspection_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_token_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_processors.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_token_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_fetch_relying_parties.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_introspection_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_introspection_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_token_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_request.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_no_consent.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_revocation_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_11_user_access_history.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_12_init.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_request_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_response_settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/authz_request_view.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/connect.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/consent_page_view.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/introspection_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/revocation_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/token_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/userinfo_endpoint.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/exceptions.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/fetch_openid_providers.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0002_rename_issuer_oidcauthentication_provider_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0003_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0004_rename_revoked_oidcauthenticationtoken_logged_out_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0005_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0006_alter_oidcauthentication_provider_configuration_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0007_alter_oidcauthentication_provider_jwks.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0008_remove_oidcauthentication_provider_and_more.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0009_alter_oidcauthenticationtoken_code.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oauth2/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oidc/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/processors.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/settings.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/css/access-button.css create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid-sp-access-button.js create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid_button.js create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb-LTS.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/favicon.ico create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/spid-ico-circle-bb.svg create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/echo_attributes.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_base.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_error.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_introspection.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_landing.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/mocked_response.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_01_rp_ops.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_02_rp_commands.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_03_rp_begin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_04_rp_callback.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_05_oidc_rpinitiated_logout.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_06_fetch_openid_providers.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_07_oidc_rp_landing.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_08_callback_echo_attributes.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_09_rp_model.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/utils.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_begin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback_echo_attributes.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_extend_session.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_initiated_logout.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_introspection.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_landing.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/admin.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/apps.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/forms.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0001_initial.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0002_alter_relyingpartytest_report.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/models.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/README.md create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/crawler.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/pyppeteer_tests.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/static/images/oops.webp create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/templates/op_user_staff_test.html create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/__init__.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/test_01_user_staff.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/urls.py create mode 100644 vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/views.py diff --git a/Dockerfile b/Dockerfile index 35ee8af..b0a04da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,15 @@ RUN apt-get update \ RUN pip install --no-cache-dir --upgrade pip \ && pip install --no-cache-dir \ - "git+https://github.com/italia/spid-cie-oidc-django.git" \ "design-django-theme==v1.4.8" WORKDIR /app +COPY vendor/spid-cie-oidc-django/spid_cie_oidc /opt/spid_cie_oidc/spid_cie_oidc +COPY vendor/spid-cie-oidc-django/setup.py /opt/spid_cie_oidc/setup.py +COPY vendor/spid-cie-oidc-django/README.md /opt/spid_cie_oidc/README.md +RUN pip install --no-cache-dir /opt/spid_cie_oidc + COPY entrypoint.sh /entrypoint.sh COPY scripts /app/scripts RUN chmod +x /entrypoint.sh diff --git a/vendor/spid-cie-oidc-django/README.md b/vendor/spid-cie-oidc-django/README.md new file mode 100644 index 0000000..8858863 --- /dev/null +++ b/vendor/spid-cie-oidc-django/README.md @@ -0,0 +1,233 @@ +# SPID/CIE OIDC Federation SDK + +![CI build](https://github.com/peppelinux/spid-cie-oidc-django/workflows/spid_cie_oidc/badge.svg) +![Python version](https://img.shields.io/badge/license-Apache%202-blue.svg) +![py-versions](https://img.shields.io/badge/python-3.10-blue.svg) +[![GitHub issues](https://img.shields.io/github/issues/italia/spid-cie-oidc-django.svg)](https://github.com/italia/spid-cie-oidc-django/issues) +[![Get invited](https://slack.developers.italia.it/badge.svg)](https://slack.developers.italia.it/) +[![Join the #spid openid](https://img.shields.io/badge/Slack%20channel-%23spid%20openid-blue.svg)](https://developersitalia.slack.com/archives/C7E85ED1N/) + + +SPID/CIE OIDC Federation is a suite of Django applications designed to +make it easy to build an [Openid Connect Federation](https://openid.net/specs/openid-connect-federation-1_0.html), +each of these can be installed separately within a django project. These are the following: + +| Application | Description | +| :--- | --- | +| __spid_cie_oidc.accounts__ | Customizable application that extends the django User model. | +| __spid_cie_oidc.entity__ | OpenID Connect Federation django app that implements OIDC Federation 1.0 Entity Statements, metadata discovery, Trust Chain, Trust Marks and Metadata policy. Technical specifications: [__OIDC Federation Entity__](docs/technical_specifications/ENTITY.md) | +| __spid_cie_oidc.authority__ | OpenID Connect Federation API and models for __OIDC Federation Trust Chain/Intermediate__, [Technical specifications](docs/technical_specifications/AUTHORITY.md) and [tutorial](docs/CREATE_A_FEDERATION.md). +| __spid_cie_oidc.onboarding__ | [__OpenID Connect Federation onboarding demo service__](docs/technical_specifications/ONBOARDING.md) and tools| +| __spid_cie_oidc.relying_party__ | [__OpenID Connect Relying Party__](docs/technical_specifications/RELYING_PARTY.md) and test suite for OIDC Providers | +| __spid_cie_oidc.provider__ | [__OpenID Connect Provider__](docs/technical_specifications/PROVIDER.md) and test suite for OIDC Relying Parties | + +## Summary + +* [Features](#features) +* [Setup](#setup) +* [Docker](#docker) +* [Usage](#usage) +* [OpenAPI Schema 3](#openapi-schema-3) +* [Tools](#tools) +* [Contribute](#contribute) + * [Contribute as end user](#contribute-as-end-user) + * [Contribute as developer](#contribute-as-developer) +* [Implementations notes](#implementation-notes) +* [License and Authors](#license-and-authors) + +------------------------------------------------ + +![RP Auth demo](docs/images/rp_auth_demo_4.gif) +_An onboarded Relying Party with a succesful authentication._ + +## Setup + +All the Django apps are available in the folder `spid_cie_oidc/`. +The examples projects are available in the folder `examples/`. + +There is a substantial difference between an app and a project. +The app is installed using a common python package manager, such as _poetry_ or _pip_, +and can be used, inherited, and integrated into other projects. + +A project is a service configuration that integrates one or more applications. +In this repository we have three example projects: + + - federation_authority + - relying_party + - provider + +Federation Authority loads all the applications for development needs, acting as both authority, SPID RP and SPID OP. +This allows us to make a demo by starting a single service. See admin page `http://127.0.0.1:8000/admin/` and user login page `http://127.0.0.1:8000/oidc/rp/landing/`. + +Then we have also another Relying Party, as indipendent project, and another Provider configured with the CIE profile. +Relying party and Provider are examples that only integrate +__spid_cie_oidc.entity__ and __spid_cie_oidc.provider__ or __.relying_party__ as applications. + +Read the [setup documentation](docs/SETUP.md) to get started. + +## Docker + +### Docker image + +```` +docker pull ghcr.io/italia/spid-cie-oidc-django:latest +```` + +### Docker compose + +Please do your customizations in each _settingslocal.py_ files and/or in the example dumps json file. + +Change hostnames from 127.0.0.1 to which one configured in the compose file, in the settingslocal.py files and in the dumps/example.json files. +In our example we rename: + +- http://127.0.0.1:8000 to http://trust-anchor.org:8000/ +- http://127.0.0.1:8001 to http://relying-party.org:8001/ +- http://127.0.0.1:8002 to http://cie-provider.org:8002/ + +We can do that with the following steps: + +- Execute `bash docker-prepare.sh` +- Customize the example data and settings contained in `examples-docker/` if needed (not necessary for a quick demo) + + +Run the stack +```` +sudo docker-compose up +```` + +Configure a proper DNS resolution for trust-anchor.org. In GNU/Linux we can configure it in `/etc/hosts`: +```` +127.0.0.1 localhost trust-anchor.org relying-party.org cie-provider.org wallet.trust-anchor.org +```` + +Point your web browser to `http://relying-party.org:8001/oidc/rp/landing` and do your first oidc authentication. + + +## Usage + +The demo proposes a small federation composed by the following entities: + + - Federation Authority, acts as trust anchor and onboarding system. It's available at `http://127.0.0.1:8000/`. It has also an embedded Spid provider and a embedded Relying Party available at `/oidc/rp/landing`. + - OpenID Relying Party, available at `http://127.0.0.1:8001/` + - CIE OpenID Provider, available at `http://127.0.0.1:8002/` + +In the docker example we have only the Federation Authority with an embedded SPID OP and a RP. + +Examples Users and Passwords: + + - __admin__ __oidcadmin__ + - __user__ __oidcuser__ + + +## OpenAPI Schema 3 + +Each application has an exportable OAS3 available at `/rest/schema.json` with a browsable reDoc UI at `/rest/api/docs`. + +![RP Auth demo](docs/images/redoc.png) +_The reDoc OAS3 browsable page._ + + +## Tools + +The OnBoarding app comes with the following collection of tools: + +- JWK + - Create a jwk + - Convert a private JWK to PEM + - Convert a public JWK to PEM + - Convert a private PEM to JWK + - Convert a public PEM to JWK + - JWT decode and verification +- Federation + - Resolve entity statement + - Apply policy +- Validators + - Validate OP metadata spid + - Validate OP metadata cie + - Validate RP metadata spid + - Validate RP metadata cie + - Validate Authn Request spid + - Validate Authn Request cie + - Validate Entity Configuration + - Trust mark validation +- Schemas + - Authorization Endpoint + - Introspection Endpoint + - Metadata + - Token Endpoint + - Revocation Endpoint + - Jwt client Assertion + +![OIDC Tools](docs/images/onboard_tools_jwt_debug.png) +_OIDC tools facilitates the lives of developers and service operators, here a simple interface to decode and verify a JWT._ + +To explore a federation on the commandline, use the [ofcli](https://github.com/dianagudu/ofcli) tool. It can be used to export federation metadata to json files for further analysis. + +## Contribute + +Your contribution is welcome, no question is useless and no answer is obvious, we need you. + +#### Contribute as end user + +Please open an issue if you've discoveerd a bug or if you want to ask some features. + +#### Contribute as developer + +Please open your Pull Requests on the __dev__ branch. +Please consider the following branches: + + - __main__: where we merge the code before tag a new stable release. + - __dev__: where we push our code during development. + - __other-custom-name__: where a new feature/contribution/bugfix will be handled, revisioned and then merged to dev branch. + +Backup and share your demo data +```` +# backup your data (upgrade example data), -e excludes. +./manage.py dumpdata -e admin -e spid_cie_oidc_relying_party -e spid_cie_oidc_provider -e spid_cie_oidc_relying_party_test -e auth -e contenttypes -e sessions --indent 2 > dumps/example.json +```` + +In this project we adopt [Semver](https://semver.org/lang/it/) and +[Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specifications. + +## Implementation notes + +All the operation related to JWT signature and encryption are built on top of [IdentityPython](https://idpy.org/) +[cryptojwt](https://github.com/IdentityPython/JWTConnect-Python-CryptoJWT) + +This project proposes an implementation of the italian OIDC Federation profile with +__automatic_client_registration__ and the adoption of the trust marks as mandatory. + +If you're looking for a fully compliant implementation of OIDC Federation 1.0, +with a full support of explicit client registration, please look at idpy's +[fedservice](https://github.com/rohe/fedservice). + +#### General Features + +- SPID and CIE OpenID Connect Provider +- SPID and CIE OpenID Connect Relying Party +- OIDC Federation onboarding demo service +- OIDC Federation 1.0 + - Trust Anchor and Intermediary + - Automatic client registration + - Entity profiles and Trust marks + - Trust chain storage and discovery + - Entity statement resolve endpoint + - Fetch statement endpoing + - List entities endpoint + - Advanced List endpoint + - Federation CLI + - RP: build trust chains for all the available OPs + - OP: build trust chains for all the available RPs +- Multitenancy, a single service can configure many entities like RPs, OP, Trust Anchors and intermediaries +- gettext compliant (i18n) +- Bootstrap Italia Design templates + +## License and Authors + +This software is released under the Apache 2 License by: + +- Giuseppe De Marco . + +In this project we use the +[metadata policy code](https://github.com/peppelinux/spid-cie-oidc-django/blob/main/spid_cie_oidc/entity/policy.py) +written by Roland Hedberg and licensed under the same Apache 2 license. diff --git a/vendor/spid-cie-oidc-django/setup.py b/vendor/spid-cie-oidc-django/setup.py new file mode 100644 index 0000000..02ee034 --- /dev/null +++ b/vendor/spid-cie-oidc-django/setup.py @@ -0,0 +1,75 @@ +import re +import os +import re +import sys + +from glob import glob +from setuptools import setup, find_packages + +SRC_FOLDER = '.' +PKG_NAME = 'spid_cie_oidc' + +INSTALL_REQUIRES = [ + "Django>=4.0,<5.0", + "cryptojwt>=1.8.2", + "pydantic>=1.8.2,<2.0", + "pytz>=2021.3", + "aiohttp>=3.9,<4", + "requests>=2.32.0,<3.0", + "pydantic[email]", + "djagger", + # Security: pin transitive deps to fix known CVEs (pip-audit) + "certifi>=2024.7.4", + "idna>=3.7", + "sqlparse>=0.4.4", + "urllib3>=2.2.0", + "setuptools>=78.1.1", +] + +with open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8') as readme: + README = readme.read() + +with open(f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}/__init__.py', 'r') as fd: + VERSION = re.search( + r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', + fd.read(), re.MULTILINE + ).group(1) + +LICENSE = "License :: OSI Approved :: Apache Software License" + +setup( + name=PKG_NAME, + version=VERSION, + description="SPID/CIE OIDC Federation Entity", + long_description=README, + long_description_content_type='text/markdown', + author='Giuseppe De Marco', + author_email='demarcog83@gmail.com', + license=LICENSE, + url=f"https://github.com/peppelinux/{PKG_NAME.replace('_', '-')}", + packages=[PKG_NAME, ], + package_dir={f"{PKG_NAME}": f"{SRC_FOLDER}/{PKG_NAME}"}, + + package_data={ + f"{PKG_NAME}": [ + i.replace( + f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}{os.path.sep}', '' + ) + for i in glob( + f'{SRC_FOLDER}{os.path.sep}{PKG_NAME}{os.path.sep}**', + recursive=True + ) + if i and '__pycache__' not in i + ] + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + LICENSE, + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries :: Python Modules"], + install_requires=INSTALL_REQUIRES, + zip_safe=False, +) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/__init__.py new file mode 100644 index 0000000..31e744e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/__init__.py @@ -0,0 +1 @@ +__version__ = "1.6.3" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin.py new file mode 100644 index 0000000..73263a5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin.py @@ -0,0 +1,74 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.admin import UserAdmin + +from .models import User + + +@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", + ) + fieldsets = ( + ( + None, + { + "fields": ( + ( + "username", + "is_active", + "is_staff", + "is_superuser", + ), + ("password"), + ("origin"), + ) + }, + ), + ( + _("Anagrafica"), + {"fields": (("first_name", "last_name"), "email", ("attributes",))}, + ), + ( + _("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) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin_inlines.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/admin_inlines.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/apps.py new file mode 100644 index 0000000..719fe11 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AccountsConfig(AppConfig): + name = "spid_cie_oidc.accounts" + verbose_name = _("User accounts") + default_auto_field = "django.db.models.BigAutoField" + label = "spid_cie_oidc_accounts" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..751f622 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0001_initial.py @@ -0,0 +1,145 @@ +# Generated by Django 4.0.2 on 2022-02-06 15:45 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + 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=96, null=True, verbose_name="Name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=96, 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", + ), + ), + ( + "origin", + models.CharField( + blank=True, + max_length=254, + null=True, + verbose_name="from which connector 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()), + ], + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0002_remove_user_taxpayer_id_user_attributes.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0002_remove_user_taxpayer_id_user_attributes.py new file mode 100644 index 0000000..5e4e52f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0002_remove_user_taxpayer_id_user_attributes.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.2 on 2022-03-02 17:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_accounts", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="user", + name="taxpayer_id", + ), + migrations.AddField( + model_name="user", + name="attributes", + field=models.JSONField(blank=True, default={}, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0003_alter_user_attributes.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0003_alter_user_attributes.py new file mode 100644 index 0000000..a639f7a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/0003_alter_user_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-03-02 17:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_accounts", "0002_remove_user_taxpayer_id_user_attributes"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="attributes", + field=models.JSONField(blank=True, default=dict, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/models.py new file mode 100644 index 0000000..f98c8d8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/models.py @@ -0,0 +1,35 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.contrib.sessions.models import Session +from django.utils.translation import gettext_lazy as _ + + +class User(AbstractUser): + first_name = models.CharField(_("Name"), max_length=96, blank=True, null=True) + last_name = models.CharField(_("Surname"), max_length=96, blank=True, null=True) + is_active = models.BooleanField(_("active"), default=True) + email = models.EmailField("email address", blank=True, null=True) + origin = models.CharField( + _("from which connector this user come from"), + max_length=254, + blank=True, + null=True, + ) + attributes = models.JSONField(default=dict, blank=True, null=True) + + class Meta: + ordering = ["username"] + verbose_name_plural = _("Users") + + def clear_sessions(self): # pragma: no cover + user_sessions = [] + for session in Session.objects.all(): + if str(self.pk) == session.get_decoded().get("_auth_user_id"): + user_sessions.append(session.pk) + return Session.objects.filter(pk__in=user_sessions).delete() + + def __str__(self): # pragma: no cover + return "{} {}".format( + self.first_name or self.attributes.get('given_name'), + self.last_name or self.attributes.get('family_name', "") + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/has_group.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/has_group.py new file mode 100644 index 0000000..4cb9439 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/accounts/templatetags/has_group.py @@ -0,0 +1,10 @@ +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): # pragma: no cover + group = Group.objects.get(name=group_name) + return group in user.groups.all() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/admin.py new file mode 100644 index 0000000..60e1eff --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin + +from .models import ( + FederationDescendant, + FederationDescendantContact, + FederationEntityProfile, + FederationEntityAssignedProfile +) + + +class FederationDescendantContactAdminInline(admin.TabularInline): + model = FederationDescendantContact + extra = 0 + readonly_fields = ("created", "modified") + raw_id_fields = ("entity",) + + +@admin.register(FederationDescendant) +class FederationDescendantAdmin(admin.ModelAdmin): + list_display = ("sub", "name", "type", "status", "is_active", "created") + list_filter = ("type", "created", "modified", "is_active") + search_fields = ("sub",) + readonly_fields = ( + "created", + "modified", + "entity_statement_preview", + ) + inlines = ( + FederationDescendantContactAdminInline, + ) + + +@admin.register(FederationEntityProfile) +class FederationEntityProfileAdmin(admin.ModelAdmin): + list_display = ("name", "profile_id") + list_filter = ("created", "modified") + search_fields = ("name", "profile_id",) + readonly_fields = ("trust_mark_template_as_json",) + + +@admin.register(FederationEntityAssignedProfile) +class FederationEntityAssignedProfileAdmin(admin.ModelAdmin): + list_display = ("descendant", "profile", "issuer", "created") + raw_id_fields = ("descendant", "profile", "issuer") + list_filter = ("created", "modified") + search_fields = ("descendant__sub", "descendant__name", "profile") + readonly_fields = ("trust_mark_as_json", "trust_mark", "created", "modified") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/apps.py new file mode 100644 index 0000000..fc0f395 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class AuthorityConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.authority" + verbose_name = "SPID/CIE OIDC Federation Authority" + label = "spid_cie_oidc_authority" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0001_initial.py new file mode 100644 index 0000000..2879073 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0001_initial.py @@ -0,0 +1,310 @@ +# Generated by Django 4.0.2 on 2022-02-13 09:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import spid_cie_oidc.authority.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("spid_cie_oidc_entity", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="FederationDescendant", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "uid", + models.CharField( + default=spid_cie_oidc.authority.models.FederationDescendant.def_uid, + help_text="an unique code that identifies this entry. For italian public service it may be the IPA code.", + max_length=1024, + unique=True, + ), + ), + ( + "name", + models.CharField( + help_text="human readable name of this entity. It may be a unit or organization name", + max_length=33, + ), + ), + ( + "sub", + models.URLField( + help_text="URL that identifies this Entity in the Federation.", + max_length=255, + unique=True, + validators=[ + spid_cie_oidc.authority.validators.validate_entity_configuration + ], + ), + ), + ( + "type", + models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ( + "metadata_policy", + models.JSONField( + blank=True, + default=dict, + help_text="if present overloads the DEFAULT policy", + ), + ), + ( + "constraints", + models.JSONField( + blank=True, + default=dict, + help_text="if present overloads the DEFAULT policy", + ), + ), + ( + "extended_claims", + models.JSONField( + blank=True, + default=dict, + help_text="a dictionary containing any other claim like: jti, crti, policy_language_crit and any other extension", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("unreachable", "unreachable"), + ("valid", "valid"), + ("signature_failed", "signature_failed"), + ("not_valid", "not_valid"), + ("unknown", "unknown"), + ("expired", "expired"), + ], + default=False, + help_text="Its entity configuration is periodically fetched and validated.", + max_length=33, + ), + ), + ( + "status_log", + models.JSONField(blank=True, default=dict, help_text="status log"), + ), + ( + "is_active", + models.BooleanField( + default=False, help_text="If this entity is active. " + ), + ), + ( + "registrant", + models.ManyToManyField( + blank=True, + help_text="Logged in users can modify only sub, contacts and jwks attributes, if they're owner of this entry. ", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Federation Entity Descendant", + "verbose_name_plural": "Federation Entity Descendants", + }, + ), + migrations.CreateModel( + name="FederationEntityProfile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("name", models.CharField(help_text="Profile name. ", max_length=33)), + ( + "profile_category", + models.CharField( + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ], + help_text="Profile id. It SHOULD be a URL but feel free to put whatever", + max_length=64, + ), + ), + ( + "profile_id", + models.CharField( + help_text="Profile id. It SHOULD be a URL but feel free to put whatever", + max_length=1024, + unique=True, + ), + ), + ( + "trust_mark_template", + models.JSONField( + blank=True, + default=dict, + help_text="trust marks template for this profile", + ), + ), + ], + options={ + "verbose_name": "Federation Entity Profile", + "verbose_name_plural": "Federation Entity Profiles", + }, + ), + migrations.CreateModel( + name="FederationEntityAssignedProfile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "descendant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + ( + "issuer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_entity.federationentityconfiguration", + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationentityprofile", + ), + ), + ], + options={ + "verbose_name": "Federation Entity Descendant Assigned Profile", + "verbose_name_plural": "Federation Descendant Assigned Profiles", + }, + ), + migrations.CreateModel( + name="FederationDescendantJwk", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "descendant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + ( + "jwk", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_entity.publicjwk", + ), + ), + ], + options={ + "verbose_name": "Federation Descendant Public JWK", + "verbose_name_plural": "Federation Descendant Public JWKs", + }, + ), + migrations.CreateModel( + name="FederationDescendantContact", + fields=[ + ( + "id", + models.BigAutoField( + 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( + help_text="any kind of contact type, usually an email.", + max_length=255, + ), + ), + ( + "type", + models.CharField( + choices=[ + ("email", "email"), + ("telephone", "telephone"), + ("other", "other"), + ], + max_length=255, + ), + ), + ( + "entity", + models.ForeignKey( + help_text="Entity for which this contac is related", + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + ], + options={ + "verbose_name": "Federation Entity Contact", + "verbose_name_plural": "Federation Entity Contacts", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0002_alter_federationentityprofile_trust_mark_template.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0002_alter_federationentityprofile_trust_mark_template.py new file mode 100644 index 0000000..7472aa7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0002_alter_federationentityprofile_trust_mark_template.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.2 on 2022-02-14 22:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_authority", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityprofile", + name="trust_mark_template", + field=models.JSONField( + default=dict, help_text="trust marks template for this profile" + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0003_alter_federationdescendantcontact_entity_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0003_alter_federationdescendantcontact_entity_and_more.py new file mode 100644 index 0000000..0c8b5e6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0003_alter_federationdescendantcontact_entity_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 4.0.2 on 2022-02-20 22:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "spid_cie_oidc_authority", + "0002_alter_federationentityprofile_trust_mark_template", + ), + ] + + operations = [ + migrations.AlterField( + model_name="federationdescendantcontact", + name="entity", + field=models.ForeignKey( + help_text="Entity for which this contac is related", + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + migrations.AlterField( + model_name="federationdescendantjwk", + name="descendant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + migrations.AlterField( + model_name="federationentityassignedprofile", + name="descendant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationdescendant", + ), + ), + migrations.AlterField( + model_name="federationentityassignedprofile", + name="profile", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_authority.federationentityprofile", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0004_federationdescendant_jwks_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0004_federationdescendant_jwks_and_more.py new file mode 100644 index 0000000..61196db --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0004_federationdescendant_jwks_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.2 on 2022-03-06 22:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0003_alter_federationdescendantcontact_entity_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='federationdescendant', + name='jwks', + field=models.JSONField(default=list, help_text='a list of public keys'), + preserve_default=False, + ), + migrations.DeleteModel( + name='FederationDescendantJwk', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0005_alter_federationdescendant_jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0005_alter_federationdescendant_jwks.py new file mode 100644 index 0000000..387dbde --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0005_alter_federationdescendant_jwks.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-06 23:11 + +from django.db import migrations, models +import spid_cie_oidc.entity.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0004_federationdescendant_jwks_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='federationdescendant', + name='jwks', + field=models.JSONField(default=list, help_text='a list of public keys', validators=[spid_cie_oidc.entity.validators.validate_public_jwks]), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0006_stafftoken.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0006_stafftoken.py new file mode 100644 index 0000000..5d9ad3d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0006_stafftoken.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.3 on 2022-03-20 10:50 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import spid_cie_oidc.authority.utils + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('spid_cie_oidc_authority', '0005_alter_federationdescendant_jwks'), + ] + + operations = [ + migrations.CreateModel( + name='StaffToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('token', models.CharField(default=spid_cie_oidc.authority.utils.random_token, help_text='any kind of contact type, usually an email.', max_length=255)), + ('is_active', models.BooleanField(default=True)), + ('user', models.ForeignKey(help_text='The user responsible of thi token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Staff Token', + 'verbose_name_plural': 'Staff Tokens', + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0007_stafftoken_expire_at.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0007_stafftoken_expire_at.py new file mode 100644 index 0000000..eb31f11 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0007_stafftoken_expire_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-03-20 10:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0006_stafftoken'), + ] + + operations = [ + migrations.AddField( + model_name='stafftoken', + name='expire_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0008_alter_stafftoken_token.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0008_alter_stafftoken_token.py new file mode 100644 index 0000000..d7b638b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0008_alter_stafftoken_token.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-28 12:04 + +from django.db import migrations, models +import spid_cie_oidc.authority.utils + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0007_stafftoken_expire_at'), + ] + + operations = [ + migrations.AlterField( + model_name='stafftoken', + name='token', + field=models.CharField(default=spid_cie_oidc.authority.utils.random_token, help_text='it will be generated automatically.', max_length=255), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0009_delete_stafftoken.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0009_delete_stafftoken.py new file mode 100644 index 0000000..9e7bac7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0009_delete_stafftoken.py @@ -0,0 +1,16 @@ +# Generated by Django 4.0.5 on 2022-10-18 09:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0008_alter_stafftoken_token'), + ] + + operations = [ + migrations.DeleteModel( + name='StaffToken', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0010_federationdescendant_metadata_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0010_federationdescendant_metadata_and_more.py new file mode 100644 index 0000000..0fafa36 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0010_federationdescendant_metadata_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-07-09 15:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_authority", "0009_delete_stafftoken"), + ] + + operations = [ + migrations.AddField( + model_name="federationdescendant", + name="metadata", + field=models.JSONField( + blank=True, + default=dict, + help_text="if present it overloads the descendants metadata, in the defined parts. This will be applied to the subordinate only.", + ), + ), + migrations.AlterField( + model_name="federationdescendant", + name="metadata_policy", + field=models.JSONField( + blank=True, + default=dict, + help_text="if present it overloads the DEFAULT policy. This will be applied recursively to all the descendants.", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0011_federationdescendant_issue_x509_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0011_federationdescendant_issue_x509_and_more.py new file mode 100644 index 0000000..63b221a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0011_federationdescendant_issue_x509_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.1 on 2023-07-09 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_authority", "0010_federationdescendant_metadata_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="federationdescendant", + name="issue_x509", + field=models.BooleanField( + default=False, + help_text="Issues x509 certificates in the entity statement.", + ), + ), + migrations.AlterField( + model_name="federationdescendant", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + migrations.AlterField( + model_name="federationentityprofile", + name="profile_category", + field=models.CharField( + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + help_text="Profile id. It SHOULD be a URL but feel free to put whatever", + max_length=64, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0012_remove_federationdescendant_issue_x509.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0012_remove_federationdescendant_issue_x509.py new file mode 100644 index 0000000..9eb133a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0012_remove_federationdescendant_issue_x509.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.1 on 2023-07-13 04:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_authority", "0011_federationdescendant_issue_x509_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="federationdescendant", + name="issue_x509", + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0013_alter_federationdescendant_type_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0013_alter_federationdescendant_type_and_more.py new file mode 100644 index 0000000..0a93f90 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0013_alter_federationdescendant_type_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.7 on 2023-11-14 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_authority", "0012_remove_federationdescendant_issue_x509"), + ] + + operations = [ + migrations.AlterField( + model_name="federationdescendant", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("openid_credential_issuer", "openid_credential_issuer"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + migrations.AlterField( + model_name="federationentityprofile", + name="profile_category", + field=models.CharField( + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("openid_credential_issuer", "openid_credential_issuer"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + help_text="Profile id. It SHOULD be a URL but feel free to put whatever", + max_length=64, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0014_alter_federationdescendant_type_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0014_alter_federationdescendant_type_and_more.py new file mode 100644 index 0000000..05646e9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/0014_alter_federationdescendant_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.5 on 2026-03-23 15:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_authority', '0013_alter_federationdescendant_type_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='federationdescendant', + name='type', + field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33), + ), + migrations.AlterField( + model_name='federationentityprofile', + name='profile_category', + field=models.CharField(choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], help_text='Profile id. It SHOULD be a URL but feel free to put whatever', max_length=64), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/models.py new file mode 100644 index 0000000..d57d673 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/models.py @@ -0,0 +1,353 @@ +from copy import deepcopy +from cryptojwt.jwk.jwk import key_from_jwk_dict + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.db import models + +# from django.db.models.signals import post_save +from django.utils.translation import gettext as _ + +from spid_cie_oidc.entity.abstract_models import TimeStampedModel +from spid_cie_oidc.entity.models import ( + ENTITY_TYPES, + ENTITY_STATUS, + FederationEntityConfiguration +) + +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.jwk_x5c import update_jwks_with_x5c +from spid_cie_oidc.entity.validators import validate_public_jwks +from spid_cie_oidc.entity.utils import iat_now, exp_from_now +from spid_cie_oidc.entity.settings import FEDERATION_DEFAULT_EXP +from spid_cie_oidc.entity.models import get_first_self_trust_anchor + +from . settings import FEDERATION_DEFAULT_POLICY +from .validators import validate_entity_configuration + +import json +import logging +import uuid + +logger = logging.getLogger(__name__) + + +class FederationEntityProfile(TimeStampedModel): + """ + Federation OnBoarding profile. + It optionally defines trust marks templates + """ + + name = models.CharField(max_length=33, help_text=_("Profile name. ")) + profile_category = models.CharField( + max_length=64, + help_text=_("Profile id. It SHOULD be a URL but feel free to put whatever"), + choices=[(i, i) for i in ENTITY_TYPES], + ) + profile_id = models.CharField( + max_length=1024, + help_text=_("Profile id. It SHOULD be a URL but feel free to put whatever"), + unique=True, + ) + trust_mark_template = models.JSONField( + help_text=_("trust marks template for this profile"), default=dict + ) + + class Meta: + verbose_name = "Federation Entity Profile" + verbose_name_plural = "Federation Entity Profiles" + + @property + def trust_mark_template_as_json(self): + return json.dumps(self.trust_mark_template) + + def __str__(self): + return f"{self.name} {self.profile_id}" + + +class FederationDescendant(TimeStampedModel): + """ + Federation OnBoarding entries. + """ + + def def_uid(): + return f"autouid-{uuid.uuid4()}" + + uid = models.CharField( + max_length=1024, + default=def_uid, + unique=True, + help_text=_( + "an unique code that identifies this entry. " + "For italian public service it may be the IPA code." + ), + ) + name = models.CharField( + max_length=33, + help_text=_( + "human readable name of this entity. " + "It may be a unit or organization name" + ), + ) + sub = models.URLField( + max_length=255, + blank=False, + null=False, + unique=True, + help_text=_("URL that identifies this Entity in the Federation."), + validators=[validate_entity_configuration], + ) + type = models.CharField( + max_length=33, + blank=True, + default="openid_relying_party", + choices=[(i, i) for i in ENTITY_TYPES], + help_text=_("OpenID Connect Federation entity type"), + ) + registrant = models.ManyToManyField( + get_user_model(), + help_text=_( + "Logged in users can modify only sub, contacts and jwks " + "attributes, if they're owner of this entry. " + ), + blank=True, + ) + jwks = models.JSONField( + blank=False, + null=False, + help_text=_("a list of public keys"), + default=list, + validators = [validate_public_jwks] + ) + metadata_policy = models.JSONField( + blank=True, help_text=_( + "if present it overloads the DEFAULT policy. This will be applied " + "recursively to all the descendants." + ), default=dict + ) + metadata = models.JSONField( + blank=True, help_text=_( + "if present it overloads the descendants metadata, " + "in the defined parts. This will be applied to the subordinate only." + ), default=dict + ) + constraints = models.JSONField( + blank=True, help_text=_("if present overloads the DEFAULT policy"), default=dict + ) + extended_claims = models.JSONField( + blank=True, + help_text=_( + "a dictionary containing any other claim like: " + "jti, crti, policy_language_crit and any other extension" + ), + default=dict, + ) + status = models.CharField( + max_length=33, + default=False, + help_text=_("Its entity configuration is periodically fetched and validated."), + choices=[(i, i) for i in ENTITY_STATUS.keys()], + ) + status_log = models.JSONField(blank=True, help_text=_("status log"), default=dict) + + is_active = models.BooleanField( + default=False, help_text=_("If this entity is active. ") + ) + + class Meta: + verbose_name = "Federation Entity Descendant" + verbose_name_plural = "Federation Entity Descendants" + + @property + def trust_marks(self): + # + profiles = FederationEntityAssignedProfile.objects.filter(descendant=self) + return [i.trust_mark for i in profiles] + + @property + def trust_marks_as_json(self): + return json.dumps(self.trust_marks) + + @property + def entity_profiles(self): + return [ + i.profile.profile_category + for i in FederationEntityAssignedProfile.objects.filter(descendant=self) + ] + + def entity_statement_as_dict(self, iss: str = None, aud: list = None) -> dict: + + ta = get_first_self_trust_anchor(iss) + private_key = key_from_jwk_dict(ta.jwks_fed[0]) + + if hasattr(settings, "X509_COMMON_NAME"): + self.jwks = update_jwks_with_x5c( + jwks = self.jwks, + private_key=private_key.private_key(), + subject = self.sub, + path_length = None, + is_ca_or_int = True + ) + + policies = { + k: FEDERATION_DEFAULT_POLICY.get(k, {}) for k in self.entity_profiles + } + + # apply custom policies if defined + policies.update(self.metadata_policy) + + data = { + "exp": exp_from_now(minutes=FEDERATION_DEFAULT_EXP), + "iat": iat_now(), + "iss": ta.sub, + "sub": self.sub, + "jwks": {"keys": self.jwks} + } + + if policies: + data["metadata_policy"] = policies + + # OpenID Federation 8.1.2: source_endpoint enables optimized refresh + if ta.fetch_endpoint: + data["source_endpoint"] = ta.fetch_endpoint + + if aud: + data["aud"] = [aud] if isinstance(aud, str) else aud + + # add contacts + contacts = FederationDescendantContact.objects.filter(entity=self).values_list( + "contact", flat=True + ) + + if contacts: + for k, v in data.get("metadata_policy", {}).items(): + if data["metadata_policy"][k].get("contacts"): + data["metadata_policy"][k]["contacts"].update( + {"add": [i for i in contacts]} + ) + else: + data["metadata_policy"][k]["contacts"] = { + "add": [i for i in contacts] + } + + # include active trust marks + tm = self.trust_marks + if tm: + data["trust_marks"] = tm + + return data + + def entity_statement_as_json(self, iss: str = None, aud: list = None) -> str: + return json.dumps(self.entity_statement_as_dict(iss, aud)) + + def entity_statement_preview(self): + return self.entity_statement_as_json() + + def entity_statement_as_jws(self, iss: str = None, aud: list = None) -> str: + issuer = get_first_self_trust_anchor(iss) + return create_jws( + self.entity_statement_as_dict(iss, aud), + issuer.jwks_fed[0], + alg=issuer.default_signature_alg, + typ="entity-statement+jwt" + ) + + def __str__(self): + return "{} [{} and {}]".format( + self.sub, self.status, "active" if self.is_active else "--" + ) + + +class FederationEntityAssignedProfile(TimeStampedModel): + descendant = models.ForeignKey(FederationDescendant, on_delete=models.CASCADE) + profile = models.ForeignKey(FederationEntityProfile, on_delete=models.CASCADE) + issuer = models.ForeignKey(FederationEntityConfiguration, on_delete=models.CASCADE) + + class Meta: + verbose_name = "Federation Entity Descendant Assigned Profile" + verbose_name_plural = "Federation Descendant Assigned Profiles" + + @property + def trust_mark_as_dict(self) -> dict: + data = deepcopy(self.profile.trust_mark_template) + data["sub"] = self.descendant.sub + data["iss"] = self.issuer.sub + data["iat"] = iat_now() + # Draft 48: trust_mark_type; retrocompat: also set trust_mark_id + data["trust_mark_type"] = self.profile.profile_id + data.setdefault("trust_mark_id", self.profile.profile_id) + return data + + @property + def trust_mark_as_json(self): + return json.dumps(self.trust_mark_as_dict) + + @property + def trust_mark_as_jws(self): + return create_jws( + self.trust_mark_as_dict, + self.issuer.jwks_fed[0], + alg=self.issuer.default_signature_alg, + typ="trust-mark+jwt" + ) + + @property + def trust_mark(self): + # Draft 48: trust_mark_type; retrocompat: also trust_mark_id and id + return { + "trust_mark_type": self.profile.profile_id, + "trust_mark": self.trust_mark_as_jws, + "trust_mark_id": self.profile.profile_id, + "id": self.profile.profile_id, + } + + def __str__(self): + return f"{self.profile} [{self.descendant}]" + + +class FederationDescendantContact(TimeStampedModel): + """ + Federation OnBoarding entries. + """ + + entity = models.ForeignKey( + FederationDescendant, + on_delete=models.CASCADE, + help_text=_("Entity for which this contac is related"), + ) + contact = models.CharField( + max_length=255, + blank=False, + null=False, + help_text=_("any kind of contact type, usually an email."), + ) + type = models.CharField( + max_length=255, + blank=False, + null=False, + choices=[(i, i) for i in ("email", "telephone", "other")], + ) + + class Meta: + verbose_name = "Federation Entity Contact" + verbose_name_plural = "Federation Entity Contacts" + + def __str__(self): + return f"{self.contact} {self.entity.sub}" + + +# signal on each save +# def trust_chain_trigger(**kwargs): +# subject = kwargs['instance'].sub + +# onboarding uses the first available configuration of federation_entity +# fe = FederationEntityConfiguration.objects.filter( +# is_active=True, +# ).first() + +# logger.info( +# f"Receiving trust chain evaluation signal for {subject}" +# ) +# return trust_chain_builder(subject) +# post_save.connect(trust_chain_trigger, sender=FederationDescendant) +# diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/advanced_entity_list_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/advanced_entity_list_endpoint.py new file mode 100644 index 0000000..4e97be9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/advanced_entity_list_endpoint.py @@ -0,0 +1,39 @@ + +from typing import List, Optional + +from pydantic import BaseModel, HttpUrl, Json + + +class AdvancedEntityListRequest(BaseModel): + page : Optional[int] + + def example(): # pragma: no cover + return AdvancedEntityListRequest( + page= 1, + ) + + +class AdvancedEntityListResponse(BaseModel): + iss: HttpUrl + iat: int + entities: List[Json] + page: int + total_pages: int + total_entries: int + next_page_path: str + prev_page_path: str + + def example(): # pragma: no cover + return AdvancedEntityListResponse( + iss= "https://registry.spid.gov.it/", + iat= 1620050972, + entities= [ + # "{'https://rp.example.it/spid': {'iat': 1588455866} }", + # "{'https://rp.another.it/spid': {'iat': 1588455866} }" + ], + page= 1, + total_pages= 2, + total_entries= 189, + next_page_path= "/federation_adv_list/?page=2", + prev_page_path= "" + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/fetch_endpoint_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/fetch_endpoint_request.py new file mode 100644 index 0000000..41e5e90 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/fetch_endpoint_request.py @@ -0,0 +1,34 @@ + +from typing import Literal, Optional + +from pydantic import BaseModel, HttpUrl, constr + + +class FetchRequest(BaseModel): + # OpenID Federation 8.1.1: sub REQUIRED; issuer implied by fetch endpoint URL + sub: HttpUrl + + def example(): # pragma: no cover + return FetchRequest(sub="http://127.0.0.1:8000/oidc/rp") + + +class FetchResponse(BaseModel): + jose : constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + + +class FedAPIErrorResponse(BaseModel): + operation: Optional[str] + error: Literal[ + "invalid_request", + "invalid_client", + "invalid_issuer", + "invalid_subject", + "invalid_trust_anchor", + "invalid_trust_chain", + "invalid_metadata", + "not_found", + "server_error", + "temporarily_unavailable", + "unsupported_parameter", + ] + error_description: str diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/list_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/list_endpoint.py new file mode 100644 index 0000000..93bfd18 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/list_endpoint.py @@ -0,0 +1,20 @@ +from typing import List, Literal, Optional, Union + +from pydantic import BaseModel, HttpUrl + + +class ListRequest(BaseModel): + # OpenID Federation 8.2.1: optional filters + entity_type: Optional[ + Literal["openid_relying_party", "openid_provider", "oauth_resource", "federation_entity"] + ] = None + trust_marked: Optional[bool] = None + trust_mark_type: Optional[Union[HttpUrl, str]] = None + intermediate: Optional[bool] = None + + def example(): # pragma: no cover + return ListRequest(entity_type="openid_provider") + + +class ListResponse(BaseModel): + response: List[HttpUrl] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/trust_mark_status_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/trust_mark_status_endpoint.py new file mode 100644 index 0000000..560e176 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/schemas/trust_mark_status_endpoint.py @@ -0,0 +1,35 @@ + +from typing import Literal, Optional + +from pydantic import BaseModel, HttpUrl, constr + +# JWT pattern (three base64url segments) to avoid flake8 F722 on constr(regex=...) +_JWT_PATTERN = r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+" + + +class TrustMarkRequest(BaseModel): + # Draft 48: trust_mark (JWT) REQUIRED for new flow + trust_mark: Optional[constr(regex=_JWT_PATTERN)] + # Retrocompat: sub + trust_mark_id / trust_mark_type / id + sub: Optional[HttpUrl] + trust_mark_id: Optional[HttpUrl] + trust_mark_type: Optional[HttpUrl] + id: Optional[HttpUrl] + + def example(): # pragma: no cover + return TrustMarkRequest( + trust_mark_type="https://www.spid.gov.it/openid-federation/agreement/op-public/", + sub="http://127.0.0.1:8000/oidc/op", + ) + + +class TrustMarkResponse(BaseModel): + active: bool + + +# OpenID Federation 8.4.2: Trust Mark Status Response JWT claims +class TrustMarkStatusResponsePayload(BaseModel): + iss: str # Entity Identifier of the issuer (Trust Mark Issuer) + iat: int + trust_mark: str # The Trust Mark JWT this status is about + status: Literal["active", "expired", "revoked", "invalid"] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/settings.py new file mode 100644 index 0000000..cb5eb39 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/settings.py @@ -0,0 +1,59 @@ +from django.conf import settings +from spid_cie_oidc.entity import settings as entity_settings + +MAX_ENTRIES_PAGE = getattr(settings, "MAX_ENTRIES_PAGE", 100) +DEFAULT_JWS_ALG = getattr(settings, "DEFAULT_JWS_ALG", entity_settings.DEFAULT_JWS_ALG) +DEFAULT_JWE_ALG = getattr(settings, "DEFAULT_JWE_ALG", entity_settings.DEFAULT_JWE_ALG) +DEFAULT_JWE_ENC = getattr(settings, "DEFAULT_JWE_ENC", entity_settings.DEFAULT_JWE_ENC) +SIGNING_ALG_VALUES_SUPPORTED = getattr( + settings, + "SIGNING_ALG_VALUES_SUPPORTED", + entity_settings.SIGNING_ALG_VALUES_SUPPORTED, +) +ENCRYPTION_ALG_VALUES_SUPPORTED = getattr( + settings, + "ENCRYPTION_ALG_VALUES_SUPPORTED", + entity_settings.ENCRYPTION_ALG_VALUES_SUPPORTED, +) +ENCRYPTION_ENC_SUPPORTED = getattr( + settings, "ENCRYPTION_ENC_SUPPORTED", entity_settings.ENCRYPTION_ENC_SUPPORTED +) + +FEDERATION_DEFAULT_POLICY = getattr( + settings, "FEDERATION_DEFAULT_POLICY", { + "openid_provider": { + "subject_types_supported": {"value": ["pairwise"]}, + "id_token_signing_alg_values_supported": { + "subset_of": SIGNING_ALG_VALUES_SUPPORTED + }, + "userinfo_signing_alg_values_supported": { + "subset_of": SIGNING_ALG_VALUES_SUPPORTED + }, + "token_endpoint_auth_methods_supported": {"value": ["private_key_jwt"]}, + "userinfo_encryption_alg_values_supported": { + "subset_of": ENCRYPTION_ALG_VALUES_SUPPORTED + }, + "userinfo_encryption_enc_values_supported": { + "subset_of": ENCRYPTION_ENC_SUPPORTED + }, + "request_object_encryption_alg_values_supported": { + "subset_of": ENCRYPTION_ALG_VALUES_SUPPORTED + }, + "request_object_encryption_enc_values_supported": { + "subset_of": ENCRYPTION_ENC_SUPPORTED + }, + "request_object_signing_alg_values_supported": { + "subset_of": SIGNING_ALG_VALUES_SUPPORTED + }, + }, + "openid_relying_party": { + "grant_types": {"subset_of": ["authorization_code", "refresh_token"]}, + "scope": { + "superset_of": ["openid"], + "subset_of": ["openid", "offline_access"], + }, + }, + "federation_entity": {}, + "oauth_resource": {}, + } +) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/mocked_responses.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/mocked_responses.py new file mode 100644 index 0000000..ec1f88b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/mocked_responses.py @@ -0,0 +1,165 @@ +from django.test import Client +from django.urls import reverse + +from spid_cie_oidc.authority.models import FederationDescendant +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.jwtse import unpad_jwt_payload, create_jws +from spid_cie_oidc.entity.exceptions import HttpError +from spid_cie_oidc.entity.tests.settings import ta_conf_data + +from .settings import rp_onboarding_data, intermediary_conf, rp_conf + +import copy +import logging + +logger = logging.getLogger(__name__) + + +class DummyContent: + def __init__(self, content): + self.content = content.encode() + self.status_code = 200 + + +class EntityResponse: + def __init__(self): + self.status_code = 200 + self.req_counter = 0 + self.client = Client() + self.result = None + + def result_as_it_is(self): + logger.info(f"Response #{self.req_counter}: {self.result.content.decode()}") + self.req_counter += 1 + return self.result.content + + def result_as_jws(self): + logger.info( + f"Response #{self.req_counter}: " + f"{unpad_jwt_payload(self.result.content.decode())}" + ) + self.req_counter += 1 + return self.result.content + + def trust_anchor_ec(self): + url = reverse("entity_configuration") + res = self.client.get(url, data={"sub": ta_conf_data["sub"]}) + return res + + def rp_ec(self): + rp = FederationEntityConfiguration.objects.get(sub=rp_onboarding_data["sub"]) + res = DummyContent(rp.entity_configuration_as_jws) + return res + + def fetch_rp_from_ta(self): + url = reverse("oidcfed_fetch") + res = self.client.get(url, data={"sub": rp_onboarding_data["sub"]}) + return res + + +class EntityResponseNoIntermediate(EntityResponse): + @property + def content(self): + if self.req_counter == 0: + self.result = self.trust_anchor_ec() + elif self.req_counter == 1: + self.result = self.rp_ec() + elif self.req_counter == 2: + self.result = self.fetch_rp_from_ta() + elif self.req_counter > 2: + raise NotImplementedError( + "The mocked resposes seems to be not aligned with the correct flow" + ) + + return self.result_as_jws() + +class EntityResponseNoIntermediateSignedJwksUri(EntityResponse): + @property + def content(self): + if self.req_counter == 0: + self.result = self.trust_anchor_ec() + elif self.req_counter == 1: + self.result = self.rp_ec() + elif self.req_counter == 2: + self.result = self.fetch_rp_from_ta() + elif self.req_counter == 3: + metadata = copy.deepcopy(rp_conf['metadata']['openid_relying_party']) + _jwks = metadata.pop('jwks') + fed_jwks = rp_conf['jwks_fed'][0] + self.result = create_jws(_jwks, fed_jwks) + return self.result.encode() + elif self.req_counter > 3: + raise NotImplementedError( + "The mocked resposes seems to be not aligned with the correct flow" + ) + + return self.result_as_jws() + + +class EntityResponseWithIntermediate(EntityResponse): + @property + def content(self): + if self.req_counter == 0: + self.result = self.trust_anchor_ec() + elif self.req_counter == 1: + self.result = self.rp_ec() + elif self.req_counter == 2: + sa = FederationEntityConfiguration.objects.get(sub=intermediary_conf["sub"]) + self.result = DummyContent(sa.entity_configuration_as_jws) + elif self.req_counter == 3: + # Subordinate statement: intermediate -> RP (not TA -> RP) + rp_desc = FederationDescendant.objects.get(sub=rp_onboarding_data["sub"]) + stmt_jws = rp_desc.entity_statement_as_jws(intermediary_conf["sub"]) + self.result = DummyContent(stmt_jws) + elif self.req_counter == 4: + url = reverse("oidcfed_fetch") + self.result = self.client.get(url, data={"sub": intermediary_conf["sub"]}) + elif self.req_counter == 5: + url = reverse("entity_configuration") + self.result = self.client.get(url, data={"sub": ta_conf_data["sub"]}) + elif self.req_counter > 5: + raise NotImplementedError( + "The mocked resposes seems to be not aligned with the correct flow" + ) + + if self.result.status_code != 200: + raise HttpError(f"Something went wrong with Http Request: {self.result.__dict__}") + + logger.info("-------------------------------------------------") + logger.info("") + return self.result_as_jws() + + +class EntityResponseWithIntermediateManyHints(EntityResponse): + @property + def content(self): + if self.req_counter == 0: + self.result = self.trust_anchor_ec() + elif self.req_counter == 1: + self.result = self.rp_ec() + elif self.req_counter == 2: + sa = FederationEntityConfiguration.objects.get(sub=intermediary_conf["sub"]) + self.result = DummyContent(sa.entity_configuration_as_jws) + elif self.req_counter == 3: + # Second authority hint (that-faulty): invalid response + self.result = DummyContent("crap") + elif self.req_counter == 4: + # Subordinate statement: intermediate -> RP + rp_desc = FederationDescendant.objects.get(sub=rp_onboarding_data["sub"]) + stmt_jws = rp_desc.entity_statement_as_jws(intermediary_conf["sub"]) + self.result = DummyContent(stmt_jws) + elif self.req_counter == 5: + # TA -> intermediate subordinate statement + url = reverse("oidcfed_fetch") + self.result = self.client.get(url, data={"sub": intermediary_conf["sub"]}) + elif self.req_counter > 5: + raise NotImplementedError( + "The mocked resposes seems to be not aligned with the correct flow" + ) + if self.result.status_code != 200: + raise HttpError(f"Something went wrong with Http Request: {self.result.__dict__}") + + try: + return self.result_as_jws() + except: + return self.result_as_it_is() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/settings.py new file mode 100644 index 0000000..4b05892 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/settings.py @@ -0,0 +1,149 @@ +from copy import deepcopy +from spid_cie_oidc.entity.jwks import serialize_key, new_rsa_key + +INTERMEDIARY_RSA = new_rsa_key() +INTERMEDIARY_JWK1 = serialize_key(INTERMEDIARY_RSA.priv_key, kind='private') +INTERMEDIARY_JWK1_pub = serialize_key(INTERMEDIARY_RSA.pub_key) + +RP_METADATA_JWK1 = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} +RP_METADATA_JWK1_pub = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} + +RP_METADATA_JWK2 = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} +RP_METADATA_JWK2_pub = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} + +rp_onboarding_data = dict( + name="RP Test", + sub="http://rp-test.it/oidc/rp/", + type="openid_relying_party", + metadata_policy={"openid_relying_party": {"scope": {"value": ["openid"]}}}, + is_active=True, + jwks = [RP_METADATA_JWK1_pub] +) + +rp_conf = { + "sub": rp_onboarding_data["sub"], + "jwks_fed" : [RP_METADATA_JWK1], + "jwks_core" : [RP_METADATA_JWK1, RP_METADATA_JWK2], + "metadata": { + "openid_relying_party": { + "application_type": "web", + "client_registration_types": ["automatic"], + "client_name": "Name of this service called http://rp-test.it/oidc/rp/", + "contacts": ["ops@rp.example.it"], + "grant_types": ["refresh_token", "authorization_code"], + "redirect_uris": ["http://rp-test.it/oidc/rp/callback/"], + "response_types": ["code"], + "subject_type": "pairwise", + "client_id": "http://rp-test.it/oidc/rp/", + "jwks": {"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]}, + } + }, + "authority_hints": ["http://testserver/"], + "is_active": True, +} + +RP_CONF_AS_JSON = { + "iss": rp_conf["sub"], + "sub": rp_conf["sub"], + "jwks": { + "keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub] + }, + "metadata": rp_conf["metadata"], + "authority_hints":rp_conf["authority_hints"] +} + +intermediary_conf = { + "sub": "http://intermediary-test", + "metadata": { + "federation_entity": { + "contacts": ["ops@localhost"], + "federation_fetch_endpoint": "http://intermediary-test/fetch", + "homepage_uri": "http://intermediary-test", + "name": "example Intermediate", + } + }, + "trust_marks": [], + "authority_hints": ["http://testserver/"], + "is_active": True, + "jwks_core": [INTERMEDIARY_JWK1], + "jwks_fed": [INTERMEDIARY_JWK1] +} +intermediary_onboarding_data = dict( + name="intermediary-test", + sub="http://intermediary-test", + type="federation_entity", + # metadata_policy = {"openid_relying_party": {"scope": {"value": ["openid"]}}}, + is_active=True, + jwks = [INTERMEDIARY_JWK1_pub] +) + + +TRUST_MARK_PAYLOAD = { + "iss": "$.issuer_sub", + "sub": "$.sub", + "iat": 1579621160, + "trust_mark_id": "https://www.spid.gov.it/certification/rp", + "logo_uri": "https://www.agid.gov.it/themes/custom/agid/logo.svg", + "ref": "https://docs.italia.it/italia/spid/spid-regole-tecniche-oidc/it/stabile/index.html", +} + +RP_PROFILE = { + "name": "SPID Public SP", + "profile_category": "openid_relying_party", + "profile_id": "https://www.spid.gov.it/certification/rp", + "trust_mark_template": TRUST_MARK_PAYLOAD, +} + +RP_METADATA = { + "openid_relying_party": { + "application_type": "web", + "client_registration_types": ["automatic"], + "client_name": f"Name of this service called {rp_onboarding_data['sub']}", + "contacts": ["ops@rp.example.it"], + "grant_types": ["refresh_token", "authorization_code"], + "jwks": {"keys": [RP_METADATA_JWK1_pub]}, + "redirect_uris": [f"{rp_onboarding_data['sub']}/callback/"], + "response_types": ["code"], + "subject_type": "pairwise", + } +} + +TA_SUB = "http://testserver.it/" + +FETCH_REQUEST = { + "sub": rp_conf["sub"], +} + +LIST_REQUEST = { + "entity_type": "openid_relying_party", +} + +TRUST_MARK = "eyJhbGciOiJSUzI1NiIsImtpZCI6IkZpZll4MDNibm9zRDhtNmdZUUlmTkhOUDljTV9TYW05VGM1bkxsb0lJcmMiLCJ0eXAiOiJ0cnVzdC1tYXJrK2p3dCJ9.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvIiwic3ViIjoiaHR0cDovLzEyNy4wLjAuMTo4MDAyL29pZGMvb3AvIiwiaWF0IjoxNjQ4NTcxMTk4LCJpZCI6Imh0dHBzOi8vd3d3LnNwaWQuZ292Lml0L2NlcnRpZmljYXRpb24vb3AiLCJtYXJrIjoiaHR0cHM6Ly93d3cuYWdpZC5nb3YuaXQvdGhlbWVzL2N1c3RvbS9hZ2lkL2xvZ28uc3ZnIiwicmVmIjoiaHR0cHM6Ly9kb2NzLml0YWxpYS5pdC9pdGFsaWEvc3BpZC9zcGlkLXJlZ29sZS10ZWNuaWNoZS1vaWRjL2l0L3N0YWJpbGUvaW5kZXguaHRtbCJ9.mN-AMOmcxOKh9w8SIw9T0N4_wsmjrnMsPp_M2k3irErz0ikrRNtOQ0iPXhJ33szXUxZQJc0jlxu3glCliViv34u6FyAUK-WyVHWf2QKHL5oPbZj45Lx6u1ZMTgU9gKEAUl6MdkGtIEU6F0-q87-BKIKTvJmErH3pdrQG7ed5fR2lvFCN7QQ4rHnVYHVMEnlZxe2oMPtRILsSHaa30yPGCYfp36BpZx9rdmrV0bb8sIsRYFin7GIhkzl4FxqEQ0J6i1A4elgP9tVQsOu1Yd4E54t62KdVtTgfI4qfmP15NoPiNZFHLIFIMsUmqHMh5nuq9KWBb2IC1dfHM6AoxCpqtQ" + +TRUST_MARK_REQUEST = { + "sub": rp_conf["sub"], + "trust_mark_id" : rp_conf["sub"], + "trust_mark" : TRUST_MARK +} + +TRUST_MARK_REQUEST_NO_SUB_ID = deepcopy(TRUST_MARK_REQUEST) +TRUST_MARK_REQUEST_NO_SUB_ID.pop("sub") +TRUST_MARK_REQUEST_NO_SUB_ID.pop("trust_mark_id") + +TRUST_MARK_REQUEST_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST) +TRUST_MARK_REQUEST_NO_TRUST_MARK.pop("trust_mark") + +TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB = deepcopy(TRUST_MARK_REQUEST) +TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB.pop("sub") + +TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST) +TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK.pop("sub") +TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK.pop("trust_mark") + +TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK = deepcopy(TRUST_MARK_REQUEST) +TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK.pop("trust_mark_id") +TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK.pop("trust_mark") + +ADVANCED_LIST_REQUEST = { + "page": 1, +} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_02_trust_anchor_intermediary.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_02_trust_anchor_intermediary.py new file mode 100644 index 0000000..b8ae8d9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_02_trust_anchor_intermediary.py @@ -0,0 +1,385 @@ +from django.test import TestCase, Client, override_settings +from django.urls import reverse + +from spid_cie_oidc.entity.exceptions import InvalidRequiredTrustMark +from spid_cie_oidc.entity.jwtse import verify_jws, unpad_jwt_head, unpad_jwt_payload +from spid_cie_oidc.entity.models import * + +from spid_cie_oidc.entity.trust_chain_operations import ( + dumps_statements_from_trust_chain_to_db, + get_or_create_trust_chain, + trust_chain_builder, +) +from spid_cie_oidc.entity.statements import ( + EntityConfiguration, + get_entity_configurations, + TrustMark, +) +from spid_cie_oidc.entity.utils import get_jwks +from spid_cie_oidc.entity.tests.settings import * + +from spid_cie_oidc.authority.models import * +from spid_cie_oidc.authority.tests.mocked_responses import * +from spid_cie_oidc.authority.tests.settings import * + + +from unittest.mock import patch + +import copy +import datetime + + +class TrustChainTest(TestCase): + def setUp(self): + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf) + + self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE) + self.rp = FederationDescendant.objects.create(**rp_onboarding_data) + + self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create( + descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf + ) + + def test_fetch_endpoint(self): + self.rp_profile.__str__() + self.rp_profile.trust_mark_template_as_json + url = reverse("oidcfed_fetch") + c = Client() + res = c.get(url, data={"sub": self.rp.sub}) + data = verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0]) + + self.assertEqual(data['source_endpoint'], 'http://testserver//fetch') + self.assertTrue(data["jwks"]) + + def test_list_endpoint(self): + url = reverse("oidcfed_list") + c = Client() + res = c.get(url, data={}) + self.assertTrue(res.json()) + self.assertEqual(res.status_code, 200) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseNoIntermediate()) + def test_trust_chain_valid_no_intermediaries(self, mocked): + + self.ta_conf.constraints = {"max_path_length": 0} + self.ta_conf.save() + + jwt = get_entity_configurations(self.ta_conf.sub) + trust_anchor_ec = EntityConfiguration(jwt[0]) + + trust_chain = trust_chain_builder( + subject=rp_onboarding_data["sub"], + trust_anchor=trust_anchor_ec + ) + self.assertTrue(trust_chain) + self.assertTrue(trust_chain.final_metadata) + + for i in trust_chain.tree_of_trust.values(): + for ec in i: + self.assertTrue(ec.is_valid) + + for ec in trust_chain.trust_path: + self.assertTrue(ec.is_valid) + + self.assertTrue(len(trust_chain.trust_path) == 2) + self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len) + + self.assertTrue(isinstance(trust_chain.exp, int)) + self.assertTrue(isinstance(trust_chain.exp_datetime, datetime.datetime)) + + def _create_federation_with_intermediary(self) -> EntityConfiguration: + jwt = get_entity_configurations(self.ta_conf.sub) + trust_anchor_ec = EntityConfiguration(jwt[0]) + + self.intermediate = FederationEntityConfiguration.objects.create( + **intermediary_conf + ) + + self.intermediate_desc = FederationDescendant.objects.create( + **intermediary_onboarding_data + ) + + FederationEntityAssignedProfile.objects.create( + descendant=self.rp, profile=self.rp_profile, issuer=self.intermediate + ) + self.rp_conf.authority_hints = [intermediary_conf["sub"]] + self.rp_conf.save() + return trust_anchor_ec + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseWithIntermediate()) + def test_trust_chain_valid_with_intermediary(self, mocked): + + trust_anchor_ec = self._create_federation_with_intermediary() + + trust_chain = trust_chain_builder( + subject=self.rp.sub, + trust_anchor=trust_anchor_ec + ) + + self.assertTrue(trust_chain) + self.assertTrue(trust_chain.final_metadata) + + for i in trust_chain.tree_of_trust.values(): + for ec in i: + self.assertTrue(ec.is_valid) + + for ec in trust_chain.trust_path: + self.assertTrue(ec.is_valid) + + self.assertTrue(len(trust_chain.trust_path) == 3) + self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len) + + tc_ser = trust_chain.serialize() + _p0 = unpad_jwt_payload(tc_ser[0]) + _p1 = unpad_jwt_payload(tc_ser[1]) + _p2 = unpad_jwt_payload(tc_ser[2]) + _p3 = unpad_jwt_payload(tc_ser[3]) + + # Entity configurations + self.assertEqual(_p0['iss'], _p0['sub']) + self.assertEqual(_p3['iss'], _p3['sub']) + + # Entity statements + self.assertNotEqual(_p1['iss'], _p1['sub']) + self.assertNotEqual(_p2['iss'], _p2['sub']) + + # Chain consistency (positive) + self.assertEqual(_p0['iss'], _p1['sub']) + self.assertEqual(_p1['iss'], _p2['sub']) + self.assertEqual(_p2['iss'], _p3['sub']) + + # Chain consistency (negative) + self.assertNotEqual(_p1['iss'], _p0['sub']) + self.assertNotEqual(_p2['iss'], _p1['sub']) + self.assertNotEqual(_p3['iss'], _p2['sub']) + + dumps = dumps_statements_from_trust_chain_to_db(trust_chain) + + self.assertTrue(isinstance(dumps, list) and len(dumps) == 5) + + self.assertTrue("sub" in dumps[0].get_entity_configuration_as_obj().payload) + + stored_trust_chain = TrustChain.objects.create( + sub=trust_chain.subject, + exp=trust_chain.exp_datetime, + chain=trust_chain.serialize(), + jwks = trust_chain.subject_configuration.jwks, + metadata=trust_chain.final_metadata, + parties_involved=[i.sub for i in trust_chain.trust_path], + status="valid", + trust_anchor=FetchedEntityStatement.objects.get( + sub=trust_anchor_ec.sub, iss=trust_anchor_ec.sub + ), + is_active=True, + ) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseWithIntermediateManyHints()) + def test_trust_chain_valid_with_intermediaries_many_authhints(self, mocked): + + trust_anchor_ec = self._create_federation_with_intermediary() + + self.rp_conf.authority_hints = [ + "http://intermediary-test", + "http://that-faulty", + ] + self.rp_conf.save() + + trust_chain = trust_chain_builder( + subject=self.rp.sub, + trust_anchor=trust_anchor_ec + ) + + for ec in trust_chain.trust_path: + self.assertTrue(ec.is_valid) + + self.assertTrue(len(trust_chain.trust_path) == 3) + self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseWithIntermediate()) + def test_trust_chain_valid_with_intermediaries_and_trust_mark_filter(self, mocked): + + trust_anchor_ec = self._create_federation_with_intermediary() + + # the RP exposes a trust marks in its entity configuration + self.rp_conf.trust_marks = [ + FederationEntityAssignedProfile.objects.filter(descendant=self.rp) + .first() + .trust_mark + ] + self.rp_conf.save() + + trust_chain = trust_chain_builder( + subject=self.rp.sub, + trust_anchor=trust_anchor_ec, + required_trust_marks=["https://www.spid.gov.it/certification/rp"], + ) + + for ec in trust_chain.trust_path: + self.assertTrue(ec.is_valid) + + self.assertTrue(trust_chain.verified_trust_marks) + self.assertTrue(isinstance(trust_chain.verified_trust_marks[0], TrustMark)) + + self.assertTrue(len(trust_chain.trust_path) == 3) + self.assertTrue((len(trust_chain.trust_path) - 2) == trust_chain.max_path_len) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseWithIntermediate()) + def test_trust_chain_with_missing_trust_mark(self, mocked): + + trust_anchor_ec = self._create_federation_with_intermediary() + + with self.assertRaises(InvalidRequiredTrustMark): + trust_chain = trust_chain_builder( + subject=self.rp.sub, + trust_anchor=trust_anchor_ec, + required_trust_marks=["https://www.spid.gov.it/certification/rp"], + ) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseNoIntermediate()) + def test_trust_chain_valid_helpers(self, mocked): + + gctc = get_or_create_trust_chain( + subject=rp_conf["sub"], + trust_anchor=self.ta_conf.sub, + # TODO + # required_trust_marks: list = [] + ) + + self.assertFalse(gctc.is_expired) + self.assertTrue(gctc.is_valid) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseNoIntermediate()) + def test_resolve_endpoint(self, mocked): + gctc = get_or_create_trust_chain( + subject=rp_conf["sub"], + trust_anchor=self.ta_conf.sub, + # TODO + # required_trust_marks: list = [] + ) + + url = reverse("oidcfed_resolve") + + c = Client() + res = c.get(url, data={ + "sub": self.rp.sub, + "trust_anchor": self.ta_conf.sub + } + ) + self.assertTrue(res.status_code == 200) + verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0]) + + def test_trust_mark_status_endpoint(self): + url = reverse("oidcfed_trust_mark_status") + c = Client() + # Legacy: sub + trust_mark_id -> 200 + JWT (application/trust-mark-status-response+jwt) + res = c.post( + url, + data={ + "trust_mark_id": self.rp_assigned_profile.profile.profile_id, + "sub": self.rp_assigned_profile.descendant.sub, + }, + ) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt") + payload = unpad_jwt_payload(res.content.decode()) + self.assertEqual(payload.get("status"), "active") + self.assertIn("trust_mark", payload) + self.assertIn("iss", payload) + self.assertIn("iat", payload) + + # Draft 48: trust_mark JWT only -> 200 + JWT + res = c.post( + url, + data={"trust_mark": self.rp_assigned_profile.trust_mark["trust_mark"]}, + ) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt") + payload = unpad_jwt_payload(res.content.decode()) + self.assertEqual(payload.get("status"), "active") + + # Legacy: GET with sub + trust_mark_id -> 200 + JWT + res = c.get( + url, + data={ + "trust_mark_id": self.rp_assigned_profile.profile.profile_id, + "sub": self.rp_assigned_profile.descendant.sub, + }, + ) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.get("Content-Type"), "application/trust-mark-status-response+jwt") + + # Empty request -> 400 invalid_request + res = c.get(url, data={}) + self.assertEqual(res.status_code, 400) + self.assertEqual(res.json().get("error"), "invalid_request") + + # Invalid trust_mark JWT -> 400 + res = c.get( + url, + data={"trust_mark": self.rp_assigned_profile.trust_mark["trust_mark"][1:]}, + ) + self.assertEqual(res.status_code, 400) + + # trust_mark_id without sub -> 400 + res = c.get( + url, + data={"trust_mark_id": self.rp_assigned_profile.profile.profile_id}, + ) + self.assertEqual(res.status_code, 400) + + # Unknown trust mark (wrong sub) -> 404 not_found + res = c.get( + url, + data={ + "trust_mark_id": self.rp_assigned_profile.profile.profile_id, + "sub": "https://nonexistent.example/", + }, + ) + self.assertEqual(res.status_code, 404) + self.assertEqual(res.json().get("error"), "not_found") + +class TrustChainWithSignedJwksUriTest(TestCase): + def setUp(self): + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + + _rp_conf = copy.deepcopy(rp_conf) + _rp_conf['metadata']['openid_relying_party'].pop('jwks') + _rp_conf['metadata']['openid_relying_party']['signed_jwks_uri'] = f"{_rp_conf['sub']}signed_jwks.jose" + self.rp_conf = FederationEntityConfiguration.objects.create(**_rp_conf) + + self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE) + self.rp = FederationDescendant.objects.create(**rp_onboarding_data) + + self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create( + descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf + ) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseNoIntermediateSignedJwksUri()) + def test_signed_jwks_uri(self, mocked): + + self.ta_conf.constraints = {"max_path_length": 0} + self.ta_conf.save() + + jwt = get_entity_configurations(self.ta_conf.sub) + trust_anchor_ec = EntityConfiguration(jwt[0]) + + trust_chain = trust_chain_builder( + subject=rp_onboarding_data["sub"], + trust_anchor=trust_anchor_ec + ) + _jwks = get_jwks( + trust_chain.final_metadata['openid_relying_party'], + trust_chain.subject_configuration.jwks + ) + self.assertTrue( + verify_jws(_jwks, self.rp_conf.jwks_fed[0]) + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_05_validators.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_05_validators.py new file mode 100644 index 0000000..6bd4252 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_05_validators.py @@ -0,0 +1,48 @@ + +from copy import deepcopy +from unittest.mock import patch + +from django.test import TestCase +from spid_cie_oidc.authority.tests.settings import ( + RP_CONF_AS_JSON, + RP_METADATA_JWK1 +) +from spid_cie_oidc.authority.utils import random_token +from spid_cie_oidc.authority.validators import validate_entity_configuration +from spid_cie_oidc.entity.exceptions import MissingAuthorityHintsClaim, NotDescendant +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.tests.settings import ta_conf_data + +JWS = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt") +RP_CONF_AS_JSON_NO_HINTS = deepcopy(RP_CONF_AS_JSON) +RP_CONF_AS_JSON_NO_HINTS.pop("authority_hints") +JWS_NO_HINTS = create_jws(RP_CONF_AS_JSON_NO_HINTS, RP_METADATA_JWK1, typ="entity-statement+jwt") +RP_CONF_AS_JSON_WRONG_HINTS = deepcopy(RP_CONF_AS_JSON) +RP_CONF_AS_JSON_WRONG_HINTS["authority_hints"] = ["http://wrong.url.it"] +JWS_WRONG_HINTS = create_jws(RP_CONF_AS_JSON_WRONG_HINTS, RP_METADATA_JWK1, typ="entity-statement+jwt") + +class ValidatorTest(TestCase): + + def setUp(self): + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + + @patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS]) + def test_validator(self, mocked): + validate_entity_configuration(["url_entity"]) + + @patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS_NO_HINTS]) + def test_validator_no_hints(self, mocked): + with self.assertRaises(MissingAuthorityHintsClaim): + validate_entity_configuration(["url_entity"]) + + @patch("spid_cie_oidc.authority.validators.get_entity_configurations", return_value = [JWS_WRONG_HINTS]) + def test_validator_no_descendant(self, mocked): + with self.assertRaises(NotDescendant): + validate_entity_configuration(["url_entity"]) + + +class UtilityTests(TestCase): + + def test_random_token(self): + self.assertTrue(random_token()) \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_06_advanced_entity_listing.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_06_advanced_entity_listing.py new file mode 100644 index 0000000..be358f7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_06_advanced_entity_listing.py @@ -0,0 +1,49 @@ +from copy import deepcopy + +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from spid_cie_oidc.authority.models import FederationDescendant +from spid_cie_oidc.authority.tests.settings import rp_onboarding_data +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.tests.settings import ta_conf_data + + +class AdvanceEntityListing(TestCase): + + def setUp(self): + FederationDescendant.objects.create(**rp_onboarding_data) + rp_onboarding_data_local = deepcopy(rp_onboarding_data) + rp_onboarding_data_local["sub"] = "http://rp-test.it/oidc/rplocal/" + FederationDescendant.objects.create(**rp_onboarding_data_local) + self.ta = FederationEntityConfiguration.objects.create(**ta_conf_data) + + @override_settings(MAX_ENTRIES_PAGE=1) + def test_advanced_entity_listing_1(self): + url = reverse("oidcfed_advanced_entity_listing") + data = {"page":2} + res = Client().get(url, data) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.json().get("iss"), "http://testserver/") + self.assertEqual(len(res.json().get("entities")), 2) + self.assertEqual(res.json().get("page"), 2) + url_prev_page = reverse("oidcfed_advanced_entity_listing") + self.assertEqual(res.json().get("prev_page_path"), f"{url_prev_page}?page=1") + + @override_settings(MAX_ENTRIES_PAGE=1) + def test_advanced_entity_listing_no_page(self): + url = reverse("oidcfed_advanced_entity_listing") + res = Client().get(url) + self.assertEqual(res.status_code, 200) + self.assertEqual(res.json().get("iss"), "http://testserver/") + self.assertEqual(len(res.json().get("entities")), 2) + self.assertEqual(res.json().get("page"), 1) + url_next_page = reverse("oidcfed_advanced_entity_listing") + self.assertEqual(res.json().get("next_page_path"), f"{url_next_page}?page=2") + + def test_advanced_entity_listing_missing_trust_anchor(self): + FederationEntityConfiguration.objects.all().delete() + c = Client() + url = reverse("oidcfed_advanced_entity_listing") + res = c.get(url) + self.assertEqual(res.status_code, 404) + self.assertEqual(res.json().get("error"), "Missing trust anchor") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_08_schemas.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_08_schemas.py new file mode 100644 index 0000000..50a0760 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/tests/test_08_schemas.py @@ -0,0 +1,51 @@ + +from django.test import TestCase +from spid_cie_oidc.authority.schemas.advanced_entity_list_endpoint import AdvancedEntityListRequest +from spid_cie_oidc.authority.schemas.fetch_endpoint_request import FetchRequest +from spid_cie_oidc.authority.schemas.list_endpoint import ListRequest +from spid_cie_oidc.authority.schemas.trust_mark_status_endpoint import TrustMarkRequest +from spid_cie_oidc.authority.tests.settings import ( + FETCH_REQUEST, + LIST_REQUEST, + TRUST_MARK_REQUEST, + ADVANCED_LIST_REQUEST, + TRUST_MARK_REQUEST_NO_SUB_ID, + TRUST_MARK_REQUEST_NO_TRUST_MARK, + TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK, + TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB, + TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK, +) + +class SchemaTest(TestCase): + + def setUp(self) -> None: + return super().setUp() + + def test_fetch_request(self): + FetchRequest(**FETCH_REQUEST) + + def test_list_request(self): + ListRequest(**LIST_REQUEST) + + def test_trust_mark_request(self): + TrustMarkRequest(**TRUST_MARK_REQUEST) + + def test_trust_mark_request_no_sub_id(self): + TrustMarkRequest(**TRUST_MARK_REQUEST_NO_SUB_ID) + + def test_trust_mark_request_no_rust_mark(self): + TrustMarkRequest(**TRUST_MARK_REQUEST_NO_TRUST_MARK) + + def test_trust_mark_request_no_sub(self): + TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB) + + def test_trust_mark_request_no_id_no_tust_mark(self): + # Schema allows optional fields; view returns 400 if neither (sub+id) nor trust_mark provided + TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_ID_NO_TRUST_MARK) + + def test_trust_mark_request_no_sub_no_tust_mark(self): + # Schema allows optional fields; view returns 400 if neither (sub+id) nor trust_mark provided + TrustMarkRequest(**TRUST_MARK_REQUEST_TRUST_MARK_NO_SUB_NO_TRUST_MARK) + + def test_advanced_list_request(self): + AdvancedEntityListRequest(**ADVANCED_LIST_REQUEST) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/urls.py new file mode 100644 index 0000000..270f44c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/urls.py @@ -0,0 +1,42 @@ +"""acsia_agent_proj URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/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.urls import path + +from .views import ( + entity_list, + fetch, + trust_mark_status, + advanced_entity_listing, + trust_marked_list +) + +_PREF = getattr(settings, "OIDC_PREFIX", "") +urlpatterns = [ + path(f"{_PREF}fetch", fetch, name="oidcfed_fetch"), + path(f"{_PREF}list", entity_list, name="oidcfed_list"), + path( + f"{_PREF}trust_mark_status", + trust_mark_status, + name="oidcfed_trust_mark_status", + ), + path( + f"{_PREF}advanced_entity_listing", + advanced_entity_listing, + name="oidcfed_advanced_entity_listing", + ), + path(f"{_PREF}trust_marked_list", trust_marked_list, name="oidcfed_tm_list"), +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/utils.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/utils.py new file mode 100644 index 0000000..a93d11d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/utils.py @@ -0,0 +1,5 @@ +from secrets import token_hex + + +def random_token(n=254): + return token_hex(n) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/validators.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/validators.py new file mode 100644 index 0000000..70885d4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/validators.py @@ -0,0 +1,60 @@ +import logging + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils.module_loading import import_string + +from spid_cie_oidc.entity.statements import ( + OIDCFED_FEDERATION_WELLKNOWN_URL, + get_entity_configurations, + EntityConfiguration, +) + +from spid_cie_oidc.entity import settings as entity_settings +from spid_cie_oidc.entity.exceptions import MissingAuthorityHintsClaim, NotDescendant + +logger = logging.getLogger(__name__) +HTTPC_PARAMS = getattr(settings, "HTTPC_PARAMS", entity_settings.HTTPC_PARAMS) + + +def validate_entity_configuration(value): + """ + value is the sub url + """ + jwt = None + try: + jwt = get_entity_configurations(value, httpc_params = HTTPC_PARAMS)[0] + except Exception as e: + raise ValidationError( + f"Failed to fetch Entity Configuration for {value}: {e}" + ) + if not jwt: # pragma: no cover + raise ValidationError( + "failed to get a valid Entity Configuration from " + f"{value}{OIDCFED_FEDERATION_WELLKNOWN_URL}" + ) + + try: + ec = EntityConfiguration(jwt, httpc_params=HTTPC_PARAMS) + ec.validate_by_itself() + except Exception as e: # pragma: no cover + raise ValidationError( + f"Failed to validate the Entity Configuration for {value}: {e}" + ) + + authority_hints = ec.payload.get("authority_hints", []) + if not authority_hints: + raise MissingAuthorityHintsClaim( + "authority_hints must be present in " + "a descendant entity configuration" + ) + + superior_sub = import_string( + "spid_cie_oidc.entity.models.get_first_self_trust_anchor" + )().sub + if superior_sub not in authority_hints: + raise NotDescendant( + f"This participant MUST have {superior_sub} in " + f"its authority_hints claim. It has: {authority_hints}." + ) + return ec diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/views.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/views.py new file mode 100644 index 0000000..3c11f51 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/authority/views.py @@ -0,0 +1,301 @@ +import logging +import math +import urllib.parse + +from djagger.decorators import schema +from django.conf import settings +from django.core.paginator import Paginator +from django.http import ( + HttpResponse, + JsonResponse, +) +from django.urls import reverse +from django.views.decorators.csrf import csrf_exempt + +from spid_cie_oidc.authority.models import ( + FederationDescendant, + FederationEntityAssignedProfile +) +from spid_cie_oidc.authority.settings import MAX_ENTRIES_PAGE +from spid_cie_oidc.entity.jwtse import ( + create_jws, + unpad_jwt_head, + unpad_jwt_payload, +) +from spid_cie_oidc.entity.models import get_first_self_trust_anchor +from spid_cie_oidc.entity.statements import get_trust_mark_type_id +from spid_cie_oidc.entity.utils import iat_now + +from . schemas.fetch_endpoint_request import FetchRequest, FedAPIErrorResponse, FetchResponse +from . schemas.list_endpoint import ListRequest, ListResponse +from . schemas.advanced_entity_list_endpoint import AdvancedEntityListRequest, AdvancedEntityListResponse +from . schemas.trust_mark_status_endpoint import ( + TrustMarkRequest, + TrustMarkResponse, +) + +logger = logging.getLogger(__name__) + + +@schema( + methods=['GET'], + get_request_schema = { + "application/x-www-form-urlencoded": FetchRequest + }, + get_response_schema = { + "400": FedAPIErrorResponse, + "404": FedAPIErrorResponse, + "200": FetchResponse + }, + tags = ['Federation API'] +) +def fetch(request): + """ + OpenID Federation 8.1: Fetch endpoint. + Request: GET with sub REQUIRED (issuer implied by this endpoint's URL). + """ + iss = get_first_self_trust_anchor() + + if not request.GET.get("sub"): + conf = get_first_self_trust_anchor() + if request.GET.get("format") == "json": + return JsonResponse(conf.entity_configuration_as_dict, safe=False) + return HttpResponse( + conf.entity_configuration_as_jws, + content_type="application/entity-statement+jwt" + ) + + sub = FederationDescendant.objects.filter( + sub=request.GET["sub"], is_active=True + ).first() + if not sub: + return JsonResponse( + { + "error": "invalid_subject", + "error_description": "entity not found" + }, + status=404 + ) + + if request.GET.get("format") == "json": + return JsonResponse( + sub.entity_statement_as_dict(iss.sub), safe=False + ) + return HttpResponse( + sub.entity_statement_as_jws(iss.sub), + content_type="application/entity-statement+jwt", + ) + + +@schema( + methods=['GET'], + get_request_schema = { + "application/x-www-form-urlencoded": ListRequest + }, + get_response_schema = { + "400": FedAPIErrorResponse, + "404": FedAPIErrorResponse, + "200": ListResponse + }, + tags = ['Federation API'] +) +def entity_list(request): + """ + OpenID Federation 8.2: Subordinate Listing. + Optional filters: entity_type, trust_marked, trust_mark_type, intermediate. + """ + _q = {"descendant__is_active": True} + + if request.GET.get("intermediate", "").lower() == "true": + _q["profile__profile_category"] = "federation_entity" + elif request.GET.get("entity_type"): + _q["profile__profile_category"] = request.GET["entity_type"] + + if request.GET.get("trust_mark_type"): + _q["profile__profile_id"] = request.GET["trust_mark_type"] + + entries = FederationEntityAssignedProfile.objects.filter(**_q).values_list( + "descendant__sub", flat=True + ) + return JsonResponse(list(set(entries)), safe=False) + + +# TODO - add the schema +# @schema( + # methods=['GET'], + # get_request_schema = { + # "application/x-www-form-urlencoded": ListRequest + # }, + # get_response_schema = { + # "400": FedAPIErrorResponse, + # "404": FedAPIErrorResponse, + # "200": ListResponse + # }, + # tags = ['Federation API'] +# ) +def trust_marked_list(request): + if request.GET.get("trust_mark_id", "").lower(): + _q = {"profile__profile_id": request.GET["trust_mark_id"]} + else: + _q = {} + + entries = FederationEntityAssignedProfile.objects.filter(**_q).values_list( + "descendant__sub", flat=True + ) + return JsonResponse(list(set(entries)), safe=False) + + +@schema( + methods=['GET'], + get_request_schema = { + "application/x-www-form-urlencoded": AdvancedEntityListRequest + }, + get_response_schema = { + "400": FedAPIErrorResponse, + "404": FedAPIErrorResponse, + "200": AdvancedEntityListResponse + }, + tags = ['Federation API'] +) +def advanced_entity_listing(request): + desecendants = FederationDescendant.objects.filter( + is_active = True, + ).order_by("-modified") + entities_list = [] + for descendant in desecendants: + try: + _ss = descendant.entity_statement_as_jws() + except AttributeError as e: + logger.warning( + f"Subordinate {descendant} missing authority hint: {e}" + ) + continue + + entity = { + descendant.sub : { + "iat" : int(descendant.modified.timestamp()), + "subordinate_statement": _ss + } + } + entities_list.append(entity) + total_entries = desecendants.count() + + _max_entries = getattr(settings, 'MAX_ENTRIES_PAGE', MAX_ENTRIES_PAGE) + p = Paginator(entities_list, _max_entries) + page = request.GET.get("page", 1) + entities = p.get_page(page) + next_page_path = "" + if entities.has_next(): + param = {"page": entities.next_page_number()} + url = f'{reverse("oidcfed_advanced_entity_listing")}?{urllib.parse.urlencode(param)}' + next_page_path = f"{url}" + prev_page_path = "" + if entities.has_previous(): + param = {"page": entities.previous_page_number()} + url = f'{reverse("oidcfed_advanced_entity_listing")}?{urllib.parse.urlencode(param)}' + prev_page_path = f"{url}" + try: + iss = get_first_self_trust_anchor().sub + except Exception: + return JsonResponse( + { + "error": "Missing trust anchor", + }, + status = 404 + ) + res = { + "iss" : iss, + "iat" : iat_now(), + "entities" : entities_list, + "page" : int(page), + "total_pages" : math.ceil(total_entries / MAX_ENTRIES_PAGE), + "total_entries" : total_entries, + "next_page_path": next_page_path, + "prev_page_path": prev_page_path, + } + return JsonResponse(res, safe=False) + + +@schema( + methods=['GET', 'POST'], + get_request_schema={ + "application/x-www-form-urlencoded": TrustMarkRequest + }, + get_response_schema={ + "400": FedAPIErrorResponse, + "404": FedAPIErrorResponse, + "200": TrustMarkResponse + }, + tags=['Federation API'] +) +@csrf_exempt +def trust_mark_status(request): + """ + OpenID Federation 8.4: Trust Mark Status endpoint. + Request: trust_mark (JWT) REQUIRED [draft 48], or legacy: sub + trust_mark_id/trust_mark_type/id. + Response: 200 application/trust-mark-status-response+jwt (signed JWT), or 404 not_found. + """ + sub = request.POST.get("sub") or request.GET.get("sub", None) + _id = ( + request.POST.get("trust_mark_type") or request.GET.get("trust_mark_type") + or request.POST.get("trust_mark_id") or request.GET.get("trust_mark_id") + or request.POST.get("id") or request.GET.get("id") + ) + trust_mark_jwt = request.POST.get("trust_mark") or request.GET.get("trust_mark", None) + + if request.method not in ("GET", "POST"): + return JsonResponse( + {"error": "invalid_request", "error_description": "Method not allowed"}, + status=400 + ) + + if trust_mark_jwt: + try: + unpad_jwt_head(trust_mark_jwt) + payload = unpad_jwt_payload(trust_mark_jwt) + sub = payload.get("sub") + _id = get_trust_mark_type_id(payload) + except Exception: + return JsonResponse( + {"error": "invalid_request", "error_description": "Invalid trust_mark JWT"}, + status=400 + ) + elif not (sub and _id): + return JsonResponse( + {"error": "invalid_request", "error_description": "trust_mark REQUIRED or sub + trust_mark_type/id"}, + status=400 + ) + + assigned = FederationEntityAssignedProfile.objects.filter( + descendant__sub=sub, + profile__profile_id=_id, + descendant__is_active=True, + ).select_related("issuer").first() + + if not assigned: + return JsonResponse( + {"error": "not_found", "error_description": "Trust Mark not found or not active"}, + status=404 + ) + + # Draft 48: response is signed JWT with iss, iat, trust_mark, status + tm_jwt_for_response = trust_mark_jwt or assigned.trust_mark.get("trust_mark") or assigned.trust_mark_as_jws + issuer = assigned.issuer + jwk = issuer.jwks_fed[0] + status_payload = { + "iss": issuer.sub, + "iat": iat_now(), + "trust_mark": tm_jwt_for_response, + "status": "active", + } + signed = create_jws( + status_payload, + jwk, + alg=issuer.default_signature_alg, + protected={"typ": "trust-mark-status-response+jwt", "kid": jwk.get("kid")}, + ) + return HttpResponse( + signed, + content_type="application/trust-mark-status-response+jwt", + status=200, + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/__init__.py new file mode 100644 index 0000000..7d77e5d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/__init__.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class KeyUsage(str, Enum): + signature = "sig" + encryption = "enc" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/abstract_models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/abstract_models.py new file mode 100644 index 0000000..e49ef46 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/abstract_models.py @@ -0,0 +1,30 @@ +from django.contrib.auth import get_user_model +from django.db import models + + +class TimeStampedModel(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + +class CreatedModifiedBy(models.Model): + created_by = models.ForeignKey( + get_user_model(), + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="%(class)s_created_by", + ) + modified_by = models.ForeignKey( + get_user_model(), + null=True, + blank=True, + on_delete=models.CASCADE, + related_name="%(class)s_modified_by", + ) + + class Meta: + abstract = True diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/admin.py new file mode 100644 index 0000000..df502e2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/admin.py @@ -0,0 +1,202 @@ +import logging + +from django.contrib import admin +from django.utils.safestring import mark_safe +from django.contrib import messages + +# from prettyjson import PrettyJSONWidget + +from django.conf import settings +from .jwtse import unpad_jwt_payload +from .statements import EntityConfiguration, get_entity_configurations, get_entity_statements +from .models import ( + FederationEntityConfiguration, + FederationHistoricalKey, + FetchedEntityStatement, + TrustChain, + StaffToken +) +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain + +from . settings import HTTPC_PARAMS + +logger = logging.getLogger(__name__) + + +@admin.register(FederationHistoricalKey) +class FederationHistoricalKey(admin.ModelAdmin): + readonly_fields = ('jwk', "as_json") + list_display = ("entity", "kid", "inactive_from") + list_filter = ("created", "modified", "inactive_from") + search_fields = ("entity",) + + +@admin.register(FederationEntityConfiguration) +class FederationEntityConfigurationAdmin(admin.ModelAdmin): + # formfield_overrides = { + # JSONField: { + # "widget": PrettyJSONWidget( + # attrs={"initial": "parsed", "disabled": True} + # ) + # } + # } + + @admin.action(description='update trust marks') + def update_trust_marks(modeladmin, request, queryset): # pragma: no cover + """ + fetch trust marks from all the authorities + """ + trust_marks = {} + + for obj in queryset: + jwts = get_entity_configurations(obj.authority_hints, HTTPC_PARAMS) + for jwt in jwts: + + try: + ec = EntityConfiguration(jwt, httpc_params=HTTPC_PARAMS) + except Exception as e: + _msg = f"Failed getting Entity Configuration for {jwt}: {e}" + logger.warning(_msg) + messages.error(_msg) + continue + + try: + # get superior fetch url + fetch_api_url = ec.payload["metadata"]["federation_entity"][ + "federation_fetch_endpoint" + ] + except KeyError: + _msg = ( + "Missing federation_fetch_endpoint in " + f"federation_entity metadata for {obj.sub} by {ec.sub}." + ) + logger.warning(_msg) + messages.error(_msg) + continue + + _url = f"{fetch_api_url}?sub={obj.sub}" + try: + logger.info(f"Getting entity statements from {_url}") + _jwts = get_entity_statements([_url], HTTPC_PARAMS) + + payload = unpad_jwt_payload(_jwts[0]) + for i in payload.get("trust_marks", []): + trust_marks[i['id']] = i['trust_mark'] + except Exception as e: + _msg = f"Error getting entity statements from {_url}: {e}" + logger.warning(_msg) + messages.error(_msg) + continue + + positions = {} + count = 0 + for i in obj.trust_marks: + positions[i['id']] = count + count += 1 + + if positions: + for k,v in trust_marks.items(): + if positions.get(k, None): + obj.trust_marks[positions[k]] = {k:v} + else: + obj.trust_marks.append({k:v}) + else: + obj.trust_marks = [ + {"trust_mark_id":k, "trust_mark":v} for k,v in trust_marks.items() + ] + + obj.save() + messages.success( + request, + f"Trust mark reloaded succesfully: {', '.join(trust_marks.keys())}" + ) + + list_display = ( + "sub", + "type", + "kids", + "is_active", + "created", + ) + list_filter = ("created", "modified", "is_active") + # search_fields = ('command__name',) + readonly_fields = ( + "created", + "modified", + "entity_configuration_as_json", + "pems_as_html", + "kids", + "type", + ) + actions = [update_trust_marks] + + def pems_as_html(self, obj): + res = "" + data = dict() + for k, v in obj.pems_as_dict.items(): + data[k] = {} + for i in ("public", "private"): + data[k][i] = v[i].replace("\n", "
") + res += ( + f"{k}

" + f"{data[k]['public']}
" + f"{data[k]['private']}

" + ) + return mark_safe(res) # nosec + + +@admin.register(TrustChain) +class TrustChainAdmin(admin.ModelAdmin): + + @admin.action(description='reload trust chain') + def update_trust_chain(modeladmin, request, queryset): # pragma: no cover + for tc in queryset: + sub = tc.sub + ta = tc.trust_anchor.sub + try : + get_or_create_trust_chain( + subject=sub, + trust_anchor=ta, + httpc_params=settings.HTTPC_PARAMS, + required_trust_marks=getattr( + settings, "OIDCFED_REQUIRED_TRUST_MARKS", [], + ), + force=True + ) + messages.success( + request, f"Trust chain successfully reloaded for {sub}" + ) + except Exception as e: + messages.error(request, f"Failed to update {sub} due to: {e}") + continue + + list_display = ("sub", "exp", "modified", "is_valid") + list_filter = ("exp", "modified", "is_active") + search_fields = ("sub",) + readonly_fields = ( + "created", + "modified", + "parties_involved", + "status", + "log", + "chain", + "iat", + ) + actions = [update_trust_chain] + + +@admin.register(FetchedEntityStatement) +class FetchedEntityStatementAdmin(admin.ModelAdmin): + list_display = ("sub", "iss", "exp", "iat", "created", "modified") + list_filter = ("created", "modified", "exp", "iat") + search_fields = ("sub", "iss") + readonly_fields = ("sub", "statement", "created", "modified", "iat", "exp", "iss") + + +@admin.register(StaffToken) +class StaffTokenAdmin(admin.ModelAdmin): + list_display = ("user", "expire_at", "is_valid") + list_filter = ("created", "modified", "expire_at") + search_fields = ("token", ) + readonly_fields = ("is_valid",) + raw_id_fields = ('user',) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/apps.py new file mode 100644 index 0000000..1772f39 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class SpidCieFederationEntityConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.entity" + verbose_name = "SPID/CIE OIDC Federation Entity" + label = "spid_cie_oidc_entity" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/exceptions.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/exceptions.py new file mode 100644 index 0000000..1ad20a5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/exceptions.py @@ -0,0 +1,53 @@ +from django.core.exceptions import ValidationError + + +class HttpError(Exception): + pass + + +class TrustChainHttpError(HttpError): + pass + + +class UnknownKid(Exception): + pass + + +class MissingJwksClaim(ValidationError): + pass + + +class MissingAuthorityHintsClaim(ValidationError): + pass + + +class NotDescendant(ValidationError): + pass + + +class TrustAnchorNeeded(ValidationError): + pass + + +class MetadataDiscoveryException(ValidationError): + pass + + +class MissingTrustMark(ValidationError): + pass + + +class InvalidRequiredTrustMark(ValidationError): + pass + + +class InvalidTrustchain(ValidationError): + pass + + +class TrustchainMissingMetadata(ValidationError): + pass + + +class InvalidEntityConfiguration(ValidationError): + pass diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/http_client.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/http_client.py new file mode 100644 index 0000000..152fa0d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/http_client.py @@ -0,0 +1,44 @@ +import aiohttp +import asyncio + + +async def fetch(session, url, httpc_params: dict = {}): + async with session.get(url, **httpc_params.get("connection", {})) as response: + if response.status != 200: # pragma: no cover + # response.raise_for_status() + return "" + return await response.text() + + +async def fetch_all(session, urls, httpc_params): + tasks = [] + for url in urls: + task = asyncio.create_task(fetch(session, url, httpc_params)) + tasks.append(task) + results = await asyncio.gather(*tasks) + return results + + +async def http_get(urls, httpc_params: dict = {}): + _con = aiohttp.TCPConnector(**httpc_params.get("connection", {})) + async with aiohttp.ClientSession( + connector=_con, + **httpc_params.get("session", {}) + ) as session: + text = await fetch_all(session, urls, httpc_params) + return text + + +if __name__ == "__main__": # pragma: no cover + httpc_params = { + "connection": {"ssl": True}, + "session": {"timeout": aiohttp.ClientTimeout(total=4)}, + } + urls = [ + "http://127.0.0.1:8001/.well-known/openid-federation", + "http://127.0.0.1:8000/.well-known/openid-federation", + "http://asdasd.it", + "http://127.0.0.1:8001/.well-known/openid-federation", + "http://google.it", + ] + asyncio.run(http_get(urls, httpc_params=httpc_params)) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwk_x5c.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwk_x5c.py new file mode 100644 index 0000000..2d0677c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwk_x5c.py @@ -0,0 +1,86 @@ +import base64 +import json +from urllib.parse import urlparse +from typing import Union + +from cryptography import x509 +from cryptography.hazmat.primitives.asymmetric import ec, ed25519, ed448, rsa +from cryptojwt.jwk.jwk import key_from_jwk_dict + +from spid_cie_oidc.entity.x509 import X509Issuer + + +def _normalize_jwks(jwks): + """ + Normalize jwks to a list of JWK dicts. Handles: + - {"keys": [...]} (JWKS structure from entity config) + - [...] (list of keys) + - Single JWK dict (as item that may be JSON string) + """ + if isinstance(jwks, dict): + if "keys" in jwks: + jwks = jwks["keys"] + else: + jwks = [jwks] + if not isinstance(jwks, list): + jwks = [jwks] + result = [] + for item in jwks: + if isinstance(item, dict): + result.append(item) + elif isinstance(item, str): + result.append(json.loads(item)) + else: + raise TypeError(f"JWK item must be dict or JSON string, got {type(item)}") + return result + + +def _public_key_from_jwk_dict(jwk_dict: dict): + """ + Extract public key from a JWK dict. Handles keys with x5c but no kty + (e.g. from entity configurations that publish keys via x5c only). + """ + if "kty" in jwk_dict: + return key_from_jwk_dict(jwk_dict).public_key() + # JWK has x5c but no kty - derive from certificate + x5c = jwk_dict.get("x5c") + if not x5c: + raise ValueError("JWK must have either kty or x5c") + der = base64.b64decode(x5c[0]) + cert = x509.load_der_x509_certificate(der) + pub_key = cert.public_key() + # Add kty so the JWK is valid for downstream consumers + if isinstance(pub_key, rsa.RSAPublicKey): + jwk_dict["kty"] = "RSA" + elif isinstance(pub_key, ec.EllipticCurvePublicKey): + jwk_dict["kty"] = "EC" + elif isinstance(pub_key, (ed25519.Ed25519PublicKey, ed448.Ed448PublicKey)): + jwk_dict["kty"] = "OKP" + else: + jwk_dict["kty"] = "RSA" # fallback for unknown types + return pub_key + + +def update_jwks_with_x5c( + jwks: list, + private_key: bytes, + subject: str, + is_ca_or_int: bool, + path_length: Union[int, None] = None, +) -> dict: + jwks = _normalize_jwks(jwks) + subject_data: dict = dict( + X509_COMMON_NAME=urlparse(subject).hostname, + # TODO: add COUNTRY_NAME, X509_STATE_OR_PROVINCE_NAME, X509_LOCALITY_NAME, X509_ORGANIZATION_NAME + entity_id=subject + ) + + for i in jwks: + i['x5c'] = X509Issuer( + private_key = private_key, + public_key = _public_key_from_jwk_dict(i), + subject_data = subject_data, + is_ca_or_int = is_ca_or_int, + path_length = path_length + ).x5c + return jwks diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwks.py new file mode 100644 index 0000000..1f255bd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwks.py @@ -0,0 +1,121 @@ +from cryptojwt.jwk.jwk import key_from_jwk_dict +from cryptojwt.jwk.rsa import new_rsa_key +from cryptojwt.jwk.ec import new_ec_key +from cryptography.hazmat.primitives import serialization +from cryptojwt.jwk.rsa import RSAKey +from cryptojwt.jwk.ec import ECKey +from cryptography.hazmat.primitives.asymmetric import rsa, ec + +from django.conf import settings + +from . import settings as local_settings + + +DEFAULT_HASH_FUNC = getattr( + settings, "DEFAULT_HASH_FUNC", local_settings.DEFAULT_HASH_FUNC +) + + +def create_jwk(key=None, hash_func=None, typ: str = getattr(settings, "PRIVATE_KEY_TYPE", None)): + if key: + key = key + elif typ and typ.lower() == 'ec': + key = new_ec_key(crv="P-256") + else: + key = new_rsa_key() + + thumbprint = key.thumbprint(hash_function=hash_func or DEFAULT_HASH_FUNC) + jwk = key.to_dict() + jwk["kid"] = thumbprint.decode() + return jwk + + +def public_jwk_from_private_jwk(jwk_dict: dict): + # exports public + _k = key_from_jwk_dict(jwk_dict) + jwk = _k.serialize() + jwk["kid"] = jwk_dict['kid'] + return jwk + + +def private_pem_from_jwk(jwk_dict: dict): + # exports private + + _k = key_from_jwk_dict(jwk_dict) + pk = _k.private_key() + pem = pk.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + return pem.decode() + + +def public_pem_from_jwk(jwk_dict: dict): + # exports private + + _k = key_from_jwk_dict(jwk_dict) + pk = _k.public_key() + cert = pk.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return cert.decode() + + +def serialize_key(key, kind="public", hash_func="SHA-256"): + """ + key can be: + - cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey + - cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey + - cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey + - cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey + - str or bytes (PEM format) + """ + data = {} + + # Handle RSA keys + if isinstance(key, rsa.RSAPublicKey): + data = {"pub_key": key} + elif isinstance(key, rsa.RSAPrivateKey): + data = {"priv_key": key} + # Handle EC keys + elif isinstance(key, ec.EllipticCurvePublicKey): + data = {"pub_key": key} + elif isinstance(key, ec.EllipticCurvePrivateKey): + data = {"priv_key": key} + # Handle PEM strings/bytes + elif isinstance(key, (str, bytes)): + if kind == "private": + if isinstance(key, rsa.RSAPrivateKey) or (isinstance(key, str) and "RSA" in key): + data = {"priv_key": private_jwk_from_pem(key)} + else: + data = {"priv_key": private_jwk_from_pem(key)} + else: + if isinstance(key, rsa.RSAPublicKey) or (isinstance(key, str) and "RSA" in key): + data = {"pub_key": public_jwk_from_pem(key)} + else: + data = {"pub_key": public_jwk_from_pem(key)} + + # Create appropriate JWK object based on key type + if isinstance(key, (rsa.RSAPublicKey, rsa.RSAPrivateKey)) or (isinstance(key, str) and "RSA" in key): + jwk_obj = RSAKey(**data) + else: + jwk_obj = ECKey(**data) + + thumbprint = jwk_obj.thumbprint(hash_function=hash_func) + jwk = jwk_obj.to_dict() + jwk["kid"] = thumbprint.hex() # Use hex() instead of decode() + return jwk + + +def private_jwk_from_pem(content:str, password:str = None): + content = content.encode() if isinstance(content, str) else content + key = serialization.load_pem_private_key(content, password=password) + return serialize_key(key, kind='private') + + +def public_jwk_from_pem(content:str, password:str = None): + content = content.encode() if isinstance(content, str) else content + key = serialization.load_pem_public_key(content) + return serialize_key(key, kind='public') diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwtse.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwtse.py new file mode 100644 index 0000000..f5d4081 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/jwtse.py @@ -0,0 +1,137 @@ +import base64 +import binascii +import json +import logging + +import cryptojwt +from cryptojwt.exception import UnsupportedAlgorithm, VerificationError +from cryptojwt.jwe.jwe import factory +from cryptojwt.jwe.jwe_ec import JWE_EC +from cryptojwt.jwe.jwe_rsa import JWE_RSA +from cryptojwt.jwk.jwk import key_from_jwk_dict +from cryptojwt.jws.jws import JWS +from cryptojwt.jws.utils import left_hash +from typing import Union + +from .settings import ( + DEFAULT_JWE_ALG, + DEFAULT_JWE_ENC, + ENCRYPTION_ALG_VALUES_SUPPORTED, + SIGNING_ALG_VALUES_SUPPORTED, +) + +logger = logging.getLogger(__name__) + + +def unpad_jwt_element(jwt: str, position: int) -> dict: + b = jwt.split(".")[position] + padded = f"{b}{'=' * divmod(len(b), 4)[1]}" + data = json.loads(base64.urlsafe_b64decode(padded)) + return data + + +def unpad_jwt_head(jwt: str) -> dict: + return unpad_jwt_element(jwt, position=0) + + +def unpad_jwt_payload(jwt: str) -> dict: + return unpad_jwt_element(jwt, position=1) + + +def create_jwe(plain_dict: Union[dict, str, int, None], jwk_dict: dict, **kwargs) -> str: + logger.debug(f"Encrypting dict as JWE: " f"{plain_dict}") + _key = key_from_jwk_dict(jwk_dict) + + if isinstance(_key, cryptojwt.jwk.rsa.RSAKey): + JWE_CLASS = JWE_RSA + elif isinstance(_key, cryptojwt.jwk.ec.ECKey): + JWE_CLASS = JWE_EC + + if isinstance(plain_dict, dict): + _payload = json.dumps(plain_dict).encode() + elif not plain_dict: + logger.warning("create_jwe with null payload!") + _payload = "" + elif isinstance(plain_dict, (str, int)): + _payload = plain_dict + else: + logger.error("create_jwe with unsupported payload type!") + _payload = "" + + _keyobj = JWE_CLASS( + _payload, + alg=DEFAULT_JWE_ALG, + enc=DEFAULT_JWE_ENC, + kid=_key.kid, + **kwargs + ) + + jwe = _keyobj.encrypt(_key.public_key()) + logger.debug(f"Encrypted dict as JWE: {jwe}") + return jwe + + +def decrypt_jwe(jwe: str, jwk_dict: dict) -> dict: + # get header + try: + jwe_header = unpad_jwt_head(jwe) + except (binascii.Error, Exception) as e: # pragma: no cover + logger.error(f"Failed to extract JWT header: {e}") + raise VerificationError("The JWT is not valid") + + _alg = jwe_header.get("alg", DEFAULT_JWE_ALG) + _enc = jwe_header.get("enc", DEFAULT_JWE_ENC) + jwe_header.get("kid") + + if _alg not in ENCRYPTION_ALG_VALUES_SUPPORTED: # pragma: no cover + raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason") + + _decryptor = factory(jwe, alg=_alg, enc=_enc) + + # _dkey = RSAKey(priv_key=PRIV_KEY) + _dkey = key_from_jwk_dict(jwk_dict) + msg = _decryptor.decrypt(jwe, [_dkey]) + + try: + msg_dict = json.loads(msg) + logger.debug(f"Decrypted JWT as: {json.dumps(msg_dict, indent=2)}") + except json.decoder.JSONDecodeError: + msg_dict = msg + logger.debug(f"Decrypted JWT as: {msg_dict}") + return msg_dict + + +def create_jws(payload: dict, jwk_dict: dict, alg: str = "RS256", protected:dict = {}, **kwargs) -> str: + _key = key_from_jwk_dict(jwk_dict) + _signer = JWS(payload, alg=alg, **kwargs) + + signature = _signer.sign_compact([_key], protected=protected, **kwargs) + return signature + + +def verify_jws(jws: str, pub_jwk: dict, **kwargs) -> str: + _key = key_from_jwk_dict(pub_jwk) + + _head = unpad_jwt_head(jws) + if _head.get("kid") != pub_jwk["kid"]: # pragma: no cover + raise Exception( + f"kid error: {_head.get('kid')} != {pub_jwk['kid']}" + ) + + _alg = _head["alg"] + if _alg not in SIGNING_ALG_VALUES_SUPPORTED or not _alg: # pragma: no cover + raise UnsupportedAlgorithm(f"{_alg} has beed disabled for security reason") + + verifier = JWS(alg=_head["alg"], **kwargs) + msg = verifier.verify_compact(jws, [_key]) + return msg + + +def verify_at_hash(id_token, access_token) -> bool: + id_token_at_hash = id_token['at_hash'] + at_hash = left_hash(access_token, "HS256") + if at_hash != id_token_at_hash: + raise Exception( + f"at_hash error: {at_hash} != {id_token_at_hash}" + ) + return True diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0001_initial.py new file mode 100644 index 0000000..ce1616c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0001_initial.py @@ -0,0 +1,243 @@ +# Generated by Django 4.0.2 on 2022-02-13 09:06 + +from django.db import migrations, models +import spid_cie_oidc.entity.models +import spid_cie_oidc.entity.validators +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="FederationEntityConfiguration", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "uuid", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ( + "sub", + models.URLField( + help_text="URL that identifies this Entity in the Federation. This value and iss are the same in the Entity Configuration.", + max_length=255, + ), + ), + ( + "default_exp", + models.PositiveIntegerField( + default=33, + help_text="how many minutes from now() an issued statement must expire", + ), + ), + ( + "default_signature_alg", + models.CharField( + default="RS256", + help_text="default signature algorithm, eg: RS256", + max_length=16, + ), + ), + ( + "authority_hints", + models.JSONField( + blank=True, + default=list, + help_text="only required if this Entity is an intermediary", + ), + ), + ( + "jwks", + models.JSONField( + default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, + help_text="a list of public keys", + ), + ), + ( + "trust_marks", + models.JSONField( + blank=True, + default=list, + help_text="which trust marks MUST be exposed in its entity configuration", + ), + ), + ( + "trust_mark_issuers", + models.JSONField( + blank=True, + default=dict, + help_text='Only usable for Trust Anchors and intermediates. Which issuers are allowed to issue trust marks for the descendants. Example: {"https://www.spid.gov.it/certification/rp": ["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}', + ), + ), + ( + "metadata", + models.JSONField( + default=dict, + help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}', + validators=[ + spid_cie_oidc.entity.validators.validate_entity_metadata + ], + ), + ), + ( + "is_active", + models.BooleanField( + default=False, + help_text="If this configuration is active. At least one configuration must be active", + ), + ), + ], + options={ + "verbose_name": "Federation Entity Configuration", + "verbose_name_plural": "Federation Entity Configurations", + }, + ), + migrations.CreateModel( + name="PublicJwk", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "kid", + models.CharField( + help_text="unique code that identifies this jwks. ", + max_length=1024, + unique=True, + ), + ), + ( + "jwk", + models.JSONField( + default=dict, + help_text="Public jwk", + validators=[ + spid_cie_oidc.entity.validators.validate_public_jwks + ], + ), + ), + ], + options={ + "verbose_name": "Public JWKs", + "verbose_name_plural": "Public JWKs", + }, + ), + migrations.CreateModel( + name="TrustChain", + fields=[ + ( + "id", + models.BigAutoField( + 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.URLField( + help_text="URL that identifies this Entity in the Federation. ", + max_length=255, + ), + ), + ( + "type", + models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ], + default="openid_provider", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ("exp", models.DateTimeField()), + ("iat", models.DateTimeField()), + ( + "chain", + models.JSONField( + blank=True, + default=list, + help_text="A list of entity statements collected during the metadata discovery", + ), + ), + ( + "resultant_metadata", + models.JSONField( + default=dict, + help_text="The final metadata applied with the metadata policy built over the chain", + ), + ), + ( + "parties_involved", + models.JSONField( + blank=True, + default=list, + help_text="subjects involved in the metadata discovery", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("unreachable", "unreachable"), + ("valid", "valid"), + ("signature_failed", "signature_failed"), + ("not_valid", "not_valid"), + ("unknown", "unknown"), + ("expired", "expired"), + ], + default=False, + help_text="Status of this trust chain, on each update.", + max_length=33, + ), + ), + ( + "status_log", + models.JSONField(blank=True, default=dict, help_text="status log"), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="If you need to disable the trust to this subject, deacticate this", + ), + ), + ], + options={ + "verbose_name": "Trust Chain", + "verbose_name_plural": "Trust Chains", + "unique_together": {("sub", "type")}, + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0002_fetchedentitystatement.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0002_fetchedentitystatement.py new file mode 100644 index 0000000..3b94dc3 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0002_fetchedentitystatement.py @@ -0,0 +1,53 @@ +# Generated by Django 4.0.2 on 2022-02-13 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="FetchedEntityStatement", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "iss", + models.URLField( + help_text="URL that identifies the issuer of this statement in the Federation. ", + max_length=255, + ), + ), + ( + "sub", + models.URLField( + help_text="URL that identifies this Entity in the Federation. ", + max_length=255, + ), + ), + ("exp", models.DateTimeField()), + ("iat", models.DateTimeField()), + ( + "statement", + models.JSONField(default=dict, help_text="Entity statement"), + ), + ], + options={ + "verbose_name": "Fetched Entity Statement", + "verbose_name_plural": "Fetched Entity Statement", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0003_federationentityconfiguration_constraints.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0003_federationentityconfiguration_constraints.py new file mode 100644 index 0000000..ac8eeca --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0003_federationentityconfiguration_constraints.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.2 on 2022-02-13 23:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0002_fetchedentitystatement"), + ] + + operations = [ + migrations.AddField( + model_name="federationentityconfiguration", + name="constraints", + field=models.JSONField( + default=dict, + help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n', + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0004_alter_federationentityconfiguration_authority_hints_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0004_alter_federationentityconfiguration_authority_hints_and_more.py new file mode 100644 index 0000000..f7c480d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0004_alter_federationentityconfiguration_authority_hints_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.2 on 2022-02-14 15:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0003_federationentityconfiguration_constraints"), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityconfiguration", + name="authority_hints", + field=models.JSONField( + blank=True, + default=list, + help_text="only required if this Entity is an intermediary or leaf.", + ), + ), + migrations.AlterField( + model_name="federationentityconfiguration", + name="default_exp", + field=models.PositiveIntegerField( + default=2880, + help_text="how many minutes from now() an issued statement must expire", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0005_alter_federationentityconfiguration_constraints.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0005_alter_federationentityconfiguration_constraints.py new file mode 100644 index 0000000..008533c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0005_alter_federationentityconfiguration_constraints.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.2 on 2022-02-14 23:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "spid_cie_oidc_entity", + "0004_alter_federationentityconfiguration_authority_hints_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityconfiguration", + name="constraints", + field=models.JSONField( + blank=True, + default=dict, + help_text='\n{\n "naming_constraints": {\n "permitted": [\n "https://.example.com"\n ],\n "excluded": [\n "https://east.example.com"\n ]\n },\n "max_path_length": 2\n}\n', + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0006_remove_trustchain_resultant_metadata_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0006_remove_trustchain_resultant_metadata_and_more.py new file mode 100644 index 0000000..78716a7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0006_remove_trustchain_resultant_metadata_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 4.0.2 on 2022-02-22 00:15 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "spid_cie_oidc_entity", + "0005_alter_federationentityconfiguration_constraints", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="trustchain", + name="resultant_metadata", + ), + migrations.RemoveField( + model_name="trustchain", + name="status_log", + ), + migrations.AddField( + model_name="trustchain", + name="log", + field=models.TextField(blank=True, default="", help_text="status log"), + ), + migrations.AddField( + model_name="trustchain", + name="metadata", + field=models.JSONField( + blank=True, + default=dict, + help_text="The final metadata applied with the metadata policy built over the chain", + null=True, + ), + ), + migrations.AddField( + model_name="trustchain", + name="processing_start", + field=models.DateTimeField( + default=django.utils.timezone.localtime, + help_text="When the metadata discovery started for this Trust Chain. It should prevent concurrent processing for the same sub/type.", + ), + ), + migrations.AlterField( + model_name="trustchain", + name="is_active", + field=models.BooleanField( + default=True, + help_text="If you need to disable the trust to this subject, uncheck this", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0007_alter_trustchain_unique_together_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0007_alter_trustchain_unique_together_and_more.py new file mode 100644 index 0000000..7ede491 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0007_alter_trustchain_unique_together_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.0.2 on 2022-02-25 14:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0006_remove_trustchain_resultant_metadata_and_more"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="trustchain", + unique_together=set(), + ), + migrations.AddField( + model_name="trustchain", + name="trust_anchor", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_entity.fetchedentitystatement", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="trustchain", + name="iat", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterUniqueTogether( + name="trustchain", + unique_together={("sub", "type", "trust_anchor")}, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0008_fetchedentitystatement_jwt.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0008_fetchedentitystatement_jwt.py new file mode 100644 index 0000000..538eb4a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0008_fetchedentitystatement_jwt.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-02-25 14:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0007_alter_trustchain_unique_together_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="fetchedentitystatement", + name="jwt", + field=models.CharField(default="", max_length=2048), + preserve_default=False, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0009_trustchain_trust_marks_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0009_trustchain_trust_marks_and_more.py new file mode 100644 index 0000000..d61837a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0009_trustchain_trust_marks_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.2 on 2022-02-26 18:26 + +from django.db import migrations, models +import spid_cie_oidc.entity.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0008_fetchedentitystatement_jwt"), + ] + + operations = [ + migrations.AddField( + model_name="trustchain", + name="trust_marks", + field=models.JSONField( + blank=True, default=list, help_text="verified trust marks" + ), + ), + migrations.AlterField( + model_name="federationentityconfiguration", + name="jwks", + field=models.JSONField( + default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, + help_text="a list of private keys", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0010_federationentityconfiguration_entity_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0010_federationentityconfiguration_entity_type.py new file mode 100644 index 0000000..d716f17 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0010_federationentityconfiguration_entity_type.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.2 on 2022-02-26 23:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0009_trustchain_trust_marks_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="federationentityconfiguration", + name="entity_type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0011_alter_trustchain_status.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0011_alter_trustchain_status.py new file mode 100644 index 0000000..ce0e1bc --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0011_alter_trustchain_status.py @@ -0,0 +1,30 @@ +# Generated by Django 4.0.2 on 2022-03-01 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_entity", "0010_federationentityconfiguration_entity_type"), + ] + + operations = [ + migrations.AlterField( + model_name="trustchain", + name="status", + field=models.CharField( + choices=[ + ("unreachable", "unreachable"), + ("valid", "valid"), + ("signature_failed", "signature_failed"), + ("not_valid", "not_valid"), + ("unknown", "unknown"), + ("expired", "expired"), + ], + default="unreachable", + help_text="Status of this trust chain, on each update.", + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0012_delete_publicjwk.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0012_delete_publicjwk.py new file mode 100644 index 0000000..beaea9d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0012_delete_publicjwk.py @@ -0,0 +1,16 @@ +# Generated by Django 4.0.2 on 2022-03-06 22:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0011_alter_trustchain_status'), + ] + + operations = [ + migrations.DeleteModel( + name='PublicJwk', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0013_alter_federationentityconfiguration_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0013_alter_federationentityconfiguration_metadata.py new file mode 100644 index 0000000..4a1f534 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0013_alter_federationentityconfiguration_metadata.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-13 00:41 + +from django.db import migrations, models +import spid_cie_oidc.entity.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0012_delete_publicjwk'), + ] + + operations = [ + migrations.AlterField( + model_name='federationentityconfiguration', + name='metadata', + field=models.JSONField(default=dict, help_text='federation_entity metadata, eg: {"federation_entity": { ... },"openid_provider": { ... },"openid_relying_party": { ... },"oauth_resource": { ... }}', validators=[spid_cie_oidc.entity.validators.validate_entity_metadata, spid_cie_oidc.entity.validators.validate_metadata_algs]), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0014_alter_trustchain_unique_together_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0014_alter_trustchain_unique_together_and_more.py new file mode 100644 index 0000000..c5801be --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0014_alter_trustchain_unique_together_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-03-20 13:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0013_alter_federationentityconfiguration_metadata'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='trustchain', + unique_together={('sub', 'trust_anchor')}, + ), + migrations.RemoveField( + model_name='trustchain', + name='type', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0015_alter_trustchain_unique_together_trustchain_type_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0015_alter_trustchain_unique_together_trustchain_type_and_more.py new file mode 100644 index 0000000..c315d15 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0015_alter_trustchain_unique_together_trustchain_type_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.0.3 on 2022-03-20 13:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0014_alter_trustchain_unique_together_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='trustchain', + unique_together=set(), + ), + migrations.AddField( + model_name='trustchain', + name='type', + field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('oauth_resource', 'oauth_resource')], default='openid_provider', help_text='OpenID Connect Federation entity type', max_length=33), + ), + migrations.AlterUniqueTogether( + name='trustchain', + unique_together={('sub', 'type', 'trust_anchor')}, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0016_alter_trustchain_unique_together_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0016_alter_trustchain_unique_together_and_more.py new file mode 100644 index 0000000..bb5e48c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0016_alter_trustchain_unique_together_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-03-20 14:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0015_alter_trustchain_unique_together_trustchain_type_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='trustchain', + unique_together={('sub', 'trust_anchor')}, + ), + migrations.RemoveField( + model_name='trustchain', + name='type', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0017_remove_federationentityconfiguration_jwks_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0017_remove_federationentityconfiguration_jwks_and_more.py new file mode 100644 index 0000000..ee3345e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0017_remove_federationentityconfiguration_jwks_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.3 on 2022-04-23 14:48 + +from django.db import migrations, models +import spid_cie_oidc.entity.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0016_alter_trustchain_unique_together_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='federationentityconfiguration', + name='jwks', + ), + migrations.AddField( + model_name='federationentityconfiguration', + name='jwks_core', + field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Core ops'), + ), + migrations.AddField( + model_name='federationentityconfiguration', + name='jwks_fed', + field=models.JSONField(default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, help_text='a list of private keys for Federation ops'), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0018_trustchain_jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0018_trustchain_jwks.py new file mode 100644 index 0000000..6b14ed7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0018_trustchain_jwks.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.5 on 2022-07-03 11:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0017_remove_federationentityconfiguration_jwks_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='trustchain', + name='jwks', + field=models.JSONField(default=[], help_text='jwks of this federation entity'), + preserve_default=False, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0019_stafftoken.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0019_stafftoken.py new file mode 100644 index 0000000..b8d2017 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0019_stafftoken.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.5 on 2022-10-18 09:08 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import spid_cie_oidc.entity.utils + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('spid_cie_oidc_entity', '0018_trustchain_jwks'), + ] + + operations = [ + migrations.CreateModel( + name='StaffToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('token', models.CharField(default=spid_cie_oidc.entity.utils.random_token, help_text='it will be generated automatically.', max_length=255)), + ('expire_at', models.DateTimeField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True)), + ('user', models.ForeignKey(help_text='The user responsible of thi token', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Staff Token', + 'verbose_name_plural': 'Staff Tokens', + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0020_alter_federationentityconfiguration_jwks_core_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0020_alter_federationentityconfiguration_jwks_core_and_more.py new file mode 100644 index 0000000..4179e1a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0020_alter_federationentityconfiguration_jwks_core_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.1 on 2023-07-08 11:35 + +from django.db import migrations, models +import spid_cie_oidc.entity.models +import spid_cie_oidc.entity.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0019_stafftoken"), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityconfiguration", + name="jwks_core", + field=models.JSONField( + blank=True, + default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, + help_text="a list of private keys for Core ops", + validators=[spid_cie_oidc.entity.validators.validate_private_jwks], + ), + ), + migrations.AlterField( + model_name="federationentityconfiguration", + name="jwks_fed", + field=models.JSONField( + default=spid_cie_oidc.entity.models.FederationEntityConfiguration._create_jwks, + help_text="a list of private keys for Federation ops", + validators=[spid_cie_oidc.entity.validators.validate_private_jwks], + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey.py new file mode 100644 index 0000000..2f7c0d2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey.py @@ -0,0 +1,86 @@ +# Generated by Django 4.2.1 on 2023-07-08 13:12 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0020_alter_federationentityconfiguration_jwks_core_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="FederationHistoricalKey", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "kid", + models.CharField( + choices=[ + (0, "unspecified"), + (1, "keyCompromise"), + (2, "cACompromise"), + (3, "affiliationChanged"), + (4, "superseded"), + (5, "cessationOfOperation"), + (6, "certificateHold"), + (8, "removeFromCRL"), + (9, "privilegeWithdrawn"), + (10, "aACompromise"), + ], + max_length=33, + ), + ), + ( + "inactive_from", + models.DateTimeField( + help_text="Expired or Revocation date if revocation motivation is configured" + ), + ), + ( + "revocation_motivation", + models.CharField( + blank=True, + choices=[ + (0, "unspecified"), + (1, "keyCompromise"), + (2, "cACompromise"), + (3, "affiliationChanged"), + (4, "superseded"), + (5, "cessationOfOperation"), + (6, "certificateHold"), + (8, "removeFromCRL"), + (9, "privilegeWithdrawn"), + (10, "aACompromise"), + ], + max_length=33, + ), + ), + ( + "entity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_entity.federationentityconfiguration", + ), + ), + ], + options={ + "verbose_name": "Federation Historical Key", + "verbose_name_plural": "Federation Historical Keys", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk.py new file mode 100644 index 0000000..6a4b509 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.1 on 2023-07-08 14:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + replaces = [ + ("spid_cie_oidc_entity", "0021_federationhistoricalkey"), + ("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"), + ("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"), + ("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"), + ("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"), + ("spid_cie_oidc_entity", "0026_alter_federationhistoricalkey_jwk"), + ] + + dependencies = [ + ( + "spid_cie_oidc_entity", + "0020_alter_federationentityconfiguration_jwks_core_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="FederationHistoricalKey", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("kid", models.CharField(max_length=128)), + ( + "inactive_from", + models.DateTimeField( + help_text="Expired or Revocation date if revocation motivation is configured" + ), + ), + ( + "revocation_motivation", + models.CharField( + blank=True, + choices=[ + (0, "unspecified"), + (1, "keyCompromise"), + (2, "cACompromise"), + (3, "affiliationChanged"), + (4, "superseded"), + (5, "cessationOfOperation"), + (6, "certificateHold"), + (8, "removeFromCRL"), + (9, "privilegeWithdrawn"), + (10, "aACompromise"), + ], + max_length=33, + ), + ), + ( + "entity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_entity.federationentityconfiguration", + ), + ), + ("jwk", models.JSONField(default=dict, help_text="private jwk")), + ], + options={ + "verbose_name": "Federation Historical Key", + "verbose_name_plural": "Federation Historical Keys", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0022_federationhistoricalkey_jwk_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0022_federationhistoricalkey_jwk_and_more.py new file mode 100644 index 0000000..fc36588 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0022_federationhistoricalkey_jwk_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-07-08 13:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0021_federationhistoricalkey"), + ] + + operations = [ + migrations.AddField( + model_name="federationhistoricalkey", + name="jwk", + field=models.TextField(default={}, help_text="private jwk"), + preserve_default=False, + ), + migrations.AlterField( + model_name="federationhistoricalkey", + name="kid", + field=models.CharField(max_length=33), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0023_alter_federationhistoricalkey_jwk.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0023_alter_federationhistoricalkey_jwk.py new file mode 100644 index 0000000..e0f943c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0023_alter_federationhistoricalkey_jwk.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-07-08 13:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0022_federationhistoricalkey_jwk_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="federationhistoricalkey", + name="jwk", + field=models.TextField(default={}, help_text="private jwk"), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0024_alter_federationhistoricalkey_kid.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0024_alter_federationhistoricalkey_kid.py new file mode 100644 index 0000000..d877c20 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0024_alter_federationhistoricalkey_kid.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-07-08 13:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0023_alter_federationhistoricalkey_jwk"), + ] + + operations = [ + migrations.AlterField( + model_name="federationhistoricalkey", + name="kid", + field=models.CharField(max_length=128), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0025_alter_federationhistoricalkey_jwk.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0025_alter_federationhistoricalkey_jwk.py new file mode 100644 index 0000000..13f69f0 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0025_alter_federationhistoricalkey_jwk.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-07-08 14:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0024_alter_federationhistoricalkey_kid"), + ] + + operations = [ + migrations.AlterField( + model_name="federationhistoricalkey", + name="jwk", + field=models.JSONField(default={}, help_text="private jwk"), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0026_alter_federationhistoricalkey_jwk.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0026_alter_federationhistoricalkey_jwk.py new file mode 100644 index 0000000..f64ee7d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0026_alter_federationhistoricalkey_jwk.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2023-07-08 14:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0025_alter_federationhistoricalkey_jwk"), + ] + + operations = [ + migrations.AlterField( + model_name="federationhistoricalkey", + name="jwk", + field=models.JSONField(default=dict, help_text="private jwk"), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0027_alter_federationhistoricalkey_revocation_motivation.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0027_alter_federationhistoricalkey_revocation_motivation.py new file mode 100644 index 0000000..7f3c8f9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0027_alter_federationhistoricalkey_revocation_motivation.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.1 on 2023-07-08 14:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0021_federationhistoricalkey_squashed_0026_alter_federationhistoricalkey_jwk", + ), + ] + + operations = [ + migrations.AlterField( + model_name="federationhistoricalkey", + name="revocation_motivation", + field=models.CharField( + blank=True, + choices=[ + ("0", "unspecified"), + ("1", "keyCompromise"), + ("2", "cACompromise"), + ("3", "affiliationChanged"), + ("4", "superseded"), + ("5", "cessationOfOperation"), + ("6", "certificateHold"), + ("8", "removeFromCRL"), + ("9", "privilegeWithdrawn"), + ("10", "aACompromise"), + ], + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0028_federationentityconfiguration_issue_x509.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0028_federationentityconfiguration_issue_x509.py new file mode 100644 index 0000000..698664d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0028_federationentityconfiguration_issue_x509.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.1 on 2023-07-09 15:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0027_alter_federationhistoricalkey_revocation_motivation", + ), + ] + + operations = [ + migrations.AddField( + model_name="federationentityconfiguration", + name="issue_x509", + field=models.BooleanField( + default=False, + help_text="If TA/Intermediate: issues x509 in both entity statements and configuration. if leaf, issues x509 certificates in configuration.", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0029_alter_federationentityconfiguration_entity_type_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0029_alter_federationentityconfiguration_entity_type_and_more.py new file mode 100644 index 0000000..c2703d8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0029_alter_federationentityconfiguration_entity_type_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.1 on 2023-07-09 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_entity", "0028_federationentityconfiguration_issue_x509"), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityconfiguration", + name="entity_type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + migrations.AlterField( + model_name="federationentityconfiguration", + name="issue_x509", + field=models.BooleanField( + default=False, + help_text="Issues x509 certificates in the Entity Configuration.", + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0030_remove_federationentityconfiguration_issue_x509.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0030_remove_federationentityconfiguration_issue_x509.py new file mode 100644 index 0000000..767077d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0030_remove_federationentityconfiguration_issue_x509.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-07-13 04:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0029_alter_federationentityconfiguration_entity_type_and_more", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="federationentityconfiguration", + name="issue_x509", + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0031_alter_federationentityconfiguration_entity_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0031_alter_federationentityconfiguration_entity_type.py new file mode 100644 index 0000000..9cf093a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0031_alter_federationentityconfiguration_entity_type.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.7 on 2023-11-14 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0030_remove_federationentityconfiguration_issue_x509", + ), + ] + + operations = [ + migrations.AlterField( + model_name="federationentityconfiguration", + name="entity_type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("openid_credential_issuer", "openid_credential_issuer"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py new file mode 100644 index 0000000..0f026fd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0032_alter_fetchedentitystatement_jwt.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.3 on 2023-12-13 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_entity", + "0031_alter_federationentityconfiguration_entity_type", + ), + ] + + operations = [ + migrations.AlterField( + model_name="fetchedentitystatement", + name="jwt", + field=models.TextField(blank=False, null=False), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0033_alter_federationentityconfiguration_entity_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0033_alter_federationentityconfiguration_entity_type.py new file mode 100644 index 0000000..6285bf6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/0033_alter_federationentityconfiguration_entity_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2026-03-23 15:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_entity', '0032_alter_fetchedentitystatement_jwt'), + ] + + operations = [ + migrations.AlterField( + model_name='federationentityconfiguration', + name='entity_type', + field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/models.py new file mode 100644 index 0000000..e7f7406 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/models.py @@ -0,0 +1,550 @@ +import json +import logging +from typing import Union +import uuid + +from django.contrib.auth import get_user_model +from django.conf import settings +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext as _ +from spid_cie_oidc.entity.abstract_models import TimeStampedModel +from cryptojwt.jwk.jwk import key_from_jwk_dict +from spid_cie_oidc.entity.jwks import ( + create_jwk, + private_pem_from_jwk, + public_pem_from_jwk, +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.settings import ( + ENTITY_STATUS, + ENTITY_TYPE_LEAFS, + ENTITY_TYPES, + FEDERATION_DEFAULT_EXP +) +from spid_cie_oidc.entity.statements import EntityConfiguration +from spid_cie_oidc.entity.utils import exp_from_now, iat_now, random_token +from spid_cie_oidc.entity.validators import ( + validate_entity_metadata, + validate_metadata_algs, + validate_private_jwks +) + +from .jwk_x5c import update_jwks_with_x5c +from .jwks import public_jwk_from_private_jwk + +logger = logging.getLogger(__name__) + + +def is_leaf(statement_metadata): + for _typ in ENTITY_TYPE_LEAFS: + if _typ in statement_metadata: + return True # pragma: no cover + + +class FederationEntityConfiguration(TimeStampedModel): + """ + Federation Authority configuration. + """ + + def _create_jwks(): + return [create_jwk()] + + uuid = models.UUIDField( + blank=False, null=False, default=uuid.uuid4, unique=True, editable=False + ) + sub = models.URLField( + max_length=255, + blank=False, + null=False, + help_text=_( + "URL that identifies this Entity in the Federation. " + "This value and iss are the same in the Entity Configuration." + ), + ) + default_exp = models.PositiveIntegerField( + default=FEDERATION_DEFAULT_EXP, + help_text=_("how many minutes from now() an issued statement must expire"), + ) + default_signature_alg = models.CharField( + max_length=16, + default=( + "ES256" if ( + hasattr(settings, "PRIVATE_KEY_TYPE") + and getattr(settings, "PRIVATE_KEY_TYPE") == "EC" + ) else "RS256" + ), + blank=False, + null=False, + help_text=_("default signature algorithm, eg: RS256"), + ) + authority_hints = models.JSONField( + blank=True, + null=False, + help_text=_("only required if this Entity is an intermediary or leaf."), + default=list, + ) + jwks_fed = models.JSONField( + blank=False, + null=False, + help_text=_("a list of private keys for Federation ops"), + default=_create_jwks, + validators = [validate_private_jwks], + ) + jwks_core = models.JSONField( + blank=True, + null=False, + help_text=_("a list of private keys for Core ops"), + default=_create_jwks, + validators = [validate_private_jwks], + ) + trust_marks = models.JSONField( + blank=True, + help_text=_("which trust marks MUST be exposed in its entity configuration"), + default=list, + ) + trust_mark_issuers = models.JSONField( + blank=True, + help_text=_( + "Only usable for Trust Anchors and intermediates. " + "Which issuers are allowed to issue trust marks for the descendants. " + 'Example: {"https://www.spid.gov.it/certification/rp": ' + '["https://registry.spid.agid.gov.it", "https://intermediary.spid.it"],' + '"https://sgd.aa.it/onboarding": ["https://sgd.aa.it", ]}' + ), + default=dict, + ) + entity_type = models.CharField( + max_length=33, + blank=True, + default="openid_relying_party", + choices=[(i, i) for i in ENTITY_TYPES], + help_text=_("OpenID Connect Federation entity type"), + ) + metadata = models.JSONField( + blank=False, + null=False, + help_text=_( + "federation_entity metadata, eg: " + '{"federation_entity": { ... },' + '"openid_provider": { ... },' + '"openid_relying_party": { ... },' + '"oauth_resource": { ... }' + "}" + ), + default=dict, + validators=[ + validate_entity_metadata, + validate_metadata_algs + ], + ) + constraints = models.JSONField( + blank=True, + help_text=_( + """ +{ + "naming_constraints": { + "permitted": [ + "https://.example.com" + ], + "excluded": [ + "https://east.example.com" + ] + }, + "max_path_length": 2 +} +""" + ), + default=dict, + # TODO + # validators=[validate_entity_metadata,] + ) + + is_active = models.BooleanField( + default=False, + help_text=_( + "If this configuration is active. " + "At least one configuration must be active" + ), + ) + + class Meta: + verbose_name = "Federation Entity Configuration" + verbose_name_plural = "Federation Entity Configurations" + + @classmethod + def get_active_conf(cls): + """ + returns the first available active acsia engine configuration found + """ + return cls.objects.filter(is_active=True).first() + + @property + def public_jwks(self): + """ + Export public JWKs from private jwks_fed. + Uses public_jwk_from_private_jwk to avoid SerializationNotPossible + when serialize_rsa_key+RSAKey.thumbprint() fails with cryptojwt. + """ + res = [] + for i in self.jwks_fed: + skey = public_jwk_from_private_jwk(i) + res.append(skey) + return res + + @property + def pems_as_dict(self): + res = {} + for i in self.jwks_fed: + res[i["kid"]] = { + "private": private_pem_from_jwk(i), + "public": public_pem_from_jwk(i), + } + return res + + @property + def pems_as_json(self): + return json.dumps(self.pems_as_dict, indent=2) + + @property + def kids(self) -> list: + return [i["kid"] for i in self.jwks_fed] + + @property + def type(self) -> list: + return [i for i in self.metadata.keys()] + + @property + def is_leaf(self): + return is_leaf(self.metadata) + + @property + def entity_configuration_as_dict(self): + conf = { + "exp": exp_from_now(self.default_exp), + "iat": iat_now(), + "iss": self.sub, + "sub": self.sub, + "jwks": {"keys": self.public_jwks}, + "metadata": self.metadata, + } + + if self.trust_mark_issuers: + conf["trust_mark_issuers"] = self.trust_mark_issuers + + if self.trust_marks: + conf["trust_marks"] = self.trust_marks + + if self.constraints: + conf["constraints"] = self.constraints + + if self.authority_hints: + conf["authority_hints"] = self.authority_hints + elif self.is_leaf: # pragma: no cover + _msg = f"Entity {self.sub} is a leaf and requires authority_hints valued" + logger.error(_msg) + + if hasattr(settings, "X509_COMMON_NAME"): + conf['jwks'] = {"keys": update_jwks_with_x5c( + jwks=self.public_jwks, + private_key=key_from_jwk_dict(self.jwks_fed[0]).private_key(), + subject=self.sub, + is_ca_or_int=True + )} + + return conf + + @property + def entity_configuration_as_json(self): + return json.dumps(self.entity_configuration_as_dict) + + @property + def entity_configuration_as_jws(self, **kwargs): + return create_jws( + self.entity_configuration_as_dict, + self.jwks_fed[0], + alg=self.default_signature_alg, + typ="entity-statement+jwt", + **kwargs, + ) + + @property + def fetch_endpoint(self) -> Union[str, None]: + metadata = self.entity_configuration_as_dict.get('metadata', {}) + if 'federation_entity' in metadata: + return metadata['federation_entity'].get("federation_fetch_endpoint", None) + + def set_jwks_as_array(self): + for i in ('jwks_fed','jwks_core'): + value = getattr(self, i) + if not isinstance(value, list): + setattr(self, i, [value]) + + def save(self, *args, **kwargs): + self.set_jwks_as_array() + super().save(*args, **kwargs) + + def __str__(self): + return "{} [{}]".format( + self.sub, "active" if self.is_active else "--" + ) + + +class FetchedEntityStatement(TimeStampedModel): + """ + Entity Statement acquired by a third party + """ + + iss = models.URLField( + max_length=255, + blank=False, + help_text=_( + "URL that identifies the issuer of this statement in the Federation. " + ), + ) + sub = models.URLField( + max_length=255, + blank=False, + help_text=_("URL that identifies this Entity in the Federation. "), + ) + exp = models.DateTimeField() + iat = models.DateTimeField() + + statement = models.JSONField( + blank=False, null=False, help_text=_("Entity statement"), default=dict + ) + jwt = models.TextField(null=False, blank=False) + + class Meta: + verbose_name = "Fetched Entity Statement" + verbose_name_plural = "Fetched Entity Statement" + + def get_entity_configuration_as_obj(self): + return EntityConfiguration(self.jwt) + + @property + def is_expired(self): + return self.exp <= timezone.localtime() + + def __str__(self): + return f"{self.sub} issued by {self.iss}" + + +class TrustChain(TimeStampedModel): + """ + Federation Trust Chain + """ + + sub = models.URLField( + max_length=255, + blank=False, + help_text=_("URL that identifies this Entity in the Federation. "), + ) + trust_anchor = models.ForeignKey(FetchedEntityStatement, on_delete=models.CASCADE) + exp = models.DateTimeField() + iat = models.DateTimeField(auto_now_add=True) + chain = models.JSONField( + blank=True, + help_text=_( + "A list of entity statements collected during the metadata discovery" + ), + default=list, + ) + jwks = models.JSONField( + blank=False, + null=False, + help_text=_("jwks of this federation entity") + ) + metadata = models.JSONField( + blank=True, + null=True, + help_text=_( + "The final metadata applied with the metadata policy built over the chain" + ), + default=dict, + ) + trust_marks = models.JSONField( + blank=True, help_text=_("verified trust marks"), default=list + ) + parties_involved = models.JSONField( + blank=True, + help_text=_("subjects involved in the metadata discovery"), + default=list, + ) + status = models.CharField( + max_length=33, + default="unreachable", + help_text=_("Status of this trust chain, on each update."), + choices=[(i, i) for i in list(ENTITY_STATUS.keys())], + ) + log = models.TextField(blank=True, help_text=_("status log"), default="") + processing_start = models.DateTimeField( + help_text=_( + "When the metadata discovery started for this Trust Chain. " + "It should prevent concurrent processing for the same sub/type." + ), + default=timezone.localtime, + ) + is_active = models.BooleanField( + default=True, + help_text=_("If you need to disable the trust to this subject, uncheck this"), + ) + + class Meta: + verbose_name = "Trust Chain" + verbose_name_plural = "Trust Chains" + unique_together = ("sub", "trust_anchor") + + @property + def subject(self): + return self.sub # pragma: no cover + + @property + def is_expired(self): + return self.exp <= timezone.localtime() + + @property + def iat_as_timestamp(self): + return int(self.iat.timestamp()) + + @property + def exp_as_timestamp(self): + return int(self.exp.timestamp()) + + @property + def is_valid(self): + return self.is_active and ENTITY_STATUS[self.status] + + def __str__(self): + return "{} [{}] [{}]".format( + self.sub, self.trust_anchor, self.is_valid + ) + + +class StaffToken(TimeStampedModel): + """ + Token provisioned to staffs operators for protected resources + """ + + user = models.ForeignKey( + get_user_model(), + on_delete=models.CASCADE, + help_text=_("The user responsible of thi token"), + ) + token = models.CharField( + max_length=255, + blank=False, + null=False, + default = random_token, + help_text=_("it will be generated automatically."), + ) + expire_at = models.DateTimeField(blank=True, null=True) + is_active = models.BooleanField( + default=True, + blank=False, + null=False + ) + + class Meta: + verbose_name = "Staff Token" + verbose_name_plural = "Staff Tokens" + + @property + def is_valid(self): + if self.is_active and not self.expire_at: + return True + elif self.is_active and self.expire_at > timezone.localtime(): + return True + else: + return False + + def __str__(self): + return f"{self.user} {self.is_active}" + + +class FederationHistoricalKey(TimeStampedModel): + """ + https://openid.net/specs/openid-connect-federation-1_0.html#name-federation-historical-keys- + """ + + REVOCATION_REASONS_MAP = ( + ("0", 'unspecified'), + ("1", 'keyCompromise'), + ("2", 'cACompromise'), + ("3", 'affiliationChanged'), + ("4", 'superseded'), + ("5", 'cessationOfOperation'), + ("6", 'certificateHold'), + # 7 is unused in rfc5280 + ("8", 'removeFromCRL'), + ("9", 'privilegeWithdrawn'), + ("10", 'aACompromise') + ) + + # REVOCATION_REASONS_CODES = {k:k for k in REVOCATION_REASONS_MAP.keys()} + + entity = models.ForeignKey(FederationEntityConfiguration, on_delete=models.CASCADE) + kid = models.CharField( + blank=False, null=False, max_length=128 + ) + inactive_from = models.DateTimeField(help_text=_( + "Expired or Revocation date if revocation motivation is configured" + )) + revocation_motivation = models.CharField( + blank=True, null=False, max_length=33, choices=REVOCATION_REASONS_MAP + ) + + jwk = models.JSONField(help_text=_("private jwk"), default=dict) + + class Meta: + verbose_name = "Federation Historical Key" + verbose_name_plural = "Federation Historical Keys" + + @property + def as_dict(self): + if not self.jwk: + return {} + + kdict = public_jwk_from_private_jwk(self.jwk) + if self.revocation_motivation: + kdict["revoked"] = { + "revoked_at": int(self.inactive_from.timestamp()), + "reason": dict(self.REVOCATION_REASONS_MAP)[self.revocation_motivation], + "reason_code": self.revocation_motivation + } + else: + kdict["exp"] = int(self.inactive_from.timestamp()) + return kdict + + @property + def as_json(self): + return json.dumps(self.as_dict) + + def save(self, *args, **kwargs): + if self.kid: + for jwk in self.entity.jwks_fed: + if not jwk.get('kid', None): + continue + elif self.kid == jwk['kid']: + _indx = self.entity.jwks_fed.index(jwk) + self.jwk = self.entity.jwks_fed.pop(_indx) + self.entity.save() + break + + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.entity} {self.kid}" + + +def get_first_self_trust_anchor( + sub: str = None, +) -> Union[FederationEntityConfiguration, None]: + """ + get the first available Trust Anchor that represent self + as a qualified issuer + """ + lk = dict(metadata__federation_entity__isnull=False, is_active=True) + if sub: + lk["sub"] = sub + return FederationEntityConfiguration.objects.filter(**lk).first() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/policy.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/policy.py new file mode 100644 index 0000000..0c308b4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/policy.py @@ -0,0 +1,355 @@ +__author__ = "Roland Hedberg" +__license__ = "Apache 2.0" +__version__ = "" + +import logging + +logger = logging.getLogger(__name__) + + +class PolicyError(Exception): + pass + + +def combine_subset_of(s1, s2): # pragma: no cover + return list(set(s1).intersection(set(s2))) + + +def combine_superset_of(s1, s2): # pragma: no cover + return list(set(s1).intersection(set(s2))) + + +def combine_one_of(s1, s2): # pragma: no cover + return list(set(s1).intersection(set(s2))) + + +def combine_add(s1, s2): # pragma: no cover + if isinstance(s1, list): + set1 = set(s1) + else: + set1 = {s1} + if isinstance(s2, list): + set2 = set(s2) + else: + set2 = {s2} + return list(set1.union(set2)) + + +POLICY_FUNCTIONS = { + "subset_of", + "superset_of", + "one_of", + "add", + "value", + "default", + "essential", +} + +OP2FUNC = { + "subset_of": combine_subset_of, + "superset_of": combine_superset_of, + "one_of": combine_one_of, + "add": combine_add, +} + + +def do_sub_one_super_add(superior, child, policy): # pragma: no cover + if policy in superior and policy in child: + comb = OP2FUNC[policy](superior[policy], child[policy]) + if comb: + return comb + else: + raise PolicyError("Value sets doesn't overlap") + elif policy in superior: + return superior[policy] + elif policy in child: + return child[policy] + + +def do_value(superior, child, policy): # pragma: no cover + if policy in superior and policy in child: + if superior[policy] == child[policy]: + return superior[policy] + else: + raise PolicyError("Not allowed to combine values") + elif policy in superior: + return superior[policy] + elif policy in child: + return child[policy] + + +def do_default(superior, child, policy): # pragma: no cover + # A child's default can not override a superiors + if policy in superior and policy in child: + if superior["default"] == child["default"]: + return superior["default"] + else: + raise PolicyError("Not allowed to change default") + elif policy in superior: + return superior[policy] + elif policy in child: + return child[policy] + + +def do_essential(superior, child, policy): # pragma: no cover + # essential: an child can make it True if a superior has states False + # but not the other way around + + if policy in superior and policy in child: + if not superior[policy] and child["essential"]: + return True + else: + return superior[policy] + elif policy in superior: + return superior[policy] + elif policy in child: # Not in superior is the same as essential=True + return True + + +DO_POLICY = { + "superset_of": do_sub_one_super_add, + "subset_of": do_sub_one_super_add, + "one_of": do_sub_one_super_add, + "add": do_sub_one_super_add, + "value": do_value, + "default": do_default, + "essential": do_essential, +} + + +def combine_claim_policy(superior, child): # noqa: C901 # pragma: no cover + """ + Combine policy rules. + Applying the child policy can only make the combined policy more restrictive. + :param superior: Superior policy + :param child: Intermediates policy + """ + # weed out everything I don't recognize + + superior_set = set(superior).intersection(POLICY_FUNCTIONS) + child_set = set(child).intersection(POLICY_FUNCTIONS) + if "value" in superior_set: # An exact value can not be restricted. + if child_set: + if "essential" in child_set: + if len(child_set) == 1: + return {"value": superior["value"], "essential": child["essential"]} + else: + raise PolicyError( + "value can only be combined with essential, not {}".format( + child_set + ) + ) + elif "value" in child_set: + if child["value"] != superior["value"]: # Not OK + raise PolicyError("Child can not set another value then superior") + else: + return superior + else: + raise PolicyError( + "Not allowed combination of policies: {} + {}".format( + superior, child + ) + ) + return superior + else: + if "essential" in superior_set and "essential" in child_set: + # can only go from False to True + if ( + superior["essential"] != child["essential"] + and child["essential"] is False + ): + raise PolicyError("Essential can not go from True to False") + + comb_policy = superior_set.union(child_set) + if "one_of" in comb_policy: + if "subset_of" in comb_policy or "superset_of" in comb_policy: + raise PolicyError( + "one_of can not be combined with subset_of/superset_of" + ) + + rule = {} + for policy in comb_policy: + rule[policy] = DO_POLICY[policy](superior, child, policy) + + if comb_policy == {"superset_of", "subset_of"}: + # make sure the subset_of is a superset of superset_of. + if set(rule["superset_of"]).difference(set(rule["subset_of"])): + raise PolicyError("superset_of not a super set of subset_of") + elif comb_policy == {"superset_of", "subset_of", "default"}: + # make sure the subset_of is a superset of superset_of. + if set(rule["superset_of"]).difference(set(rule["subset_of"])): + raise PolicyError("superset_of not a super set of subset_of") + if set(rule["default"]).difference(set(rule["subset_of"])): + raise PolicyError("default not a sub set of subset_of") + if set(rule["superset_of"]).difference(set(rule["default"])): + raise PolicyError("default not a super set of subset_of") + elif comb_policy == {"subset_of", "default"}: + if set(rule["default"]).difference(set(rule["subset_of"])): + raise PolicyError("default not a sub set of subset_of") + elif comb_policy == {"superset_of", "default"}: + if set(rule["superset_of"]).difference(set(rule["default"])): + raise PolicyError("default not a super set of subset_of") + elif comb_policy == {"one_of", "default"}: + if isinstance(rule["default"], list): + if set(rule["default"]).difference(set(rule["one_of"])): + raise PolicyError("default not a super set of one_of") + else: + if {rule["default"]}.difference(set(rule["one_of"])): + raise PolicyError("default not a super set of one_of") + return rule + + +def combine_policy(superior, child): + res = {} + sup_set = set(superior.keys()) + chi_set = set(child.keys()) + + for claim in set(sup_set).intersection(chi_set): + res[claim] = combine_claim_policy(superior[claim], child[claim]) + + for claim in sup_set.difference(chi_set): + res[claim] = superior[claim] + + for claim in chi_set.difference(sup_set): + res[claim] = child[claim] + + return res + + +def gather_policies(chain, entity_type): + """ + Gather and combine all the metadata policies that are defined in the trust chain + :param chain: A list of Entity Statements + :return: The combined metadata policy + """ + + try: + combined_policy = chain[0]["metadata_policy"][entity_type] + except KeyError: + combined_policy = {} + + for es in chain[1:]: + try: + child = es["metadata_policy"][entity_type] + except KeyError: + pass + else: + combined_policy = combine_policy(combined_policy, child) + + return combined_policy + + +def union(val1, val2): + if isinstance(val1, list): + base = set(val1) + else: + base = {val1} + + if isinstance(val2, list): + ext = set(val2) + else: + ext = {val2} + return base.union(ext) + + +def apply_policy(metadata, policy): + """ + Apply a metadata policy to a metadata statement. + The order is value, add, default and then the checks subset_of/superset_of and one_of + :param metadata: A metadata statement + :param policy: A metadata policy + :return: A metadata statement that adheres to a metadata policy + """ + metadata_set = set(metadata.keys()) + policy_set = set(policy.keys()) + + # Metadata claims that there exists a policy for + for claim in metadata_set.intersection(policy_set): + if "value" in policy[claim]: # value overrides everything + metadata[claim] = policy[claim]["value"] + else: + if "one_of" in policy[claim]: + # The is for claims that can have only one value + if isinstance(metadata[claim], list): # Should not be but ... + _claim = [ + c for c in metadata[claim] if c in policy[claim]["one_of"] + ] + if _claim: + metadata[claim] = _claim[0] + else: + raise PolicyError( + "{}: None of {} among {}".format( + claim, metadata[claim], policy[claim]["one_of"] + ) + ) + else: + if metadata[claim] in policy[claim]["one_of"]: + pass + else: + raise PolicyError( + "{} not among {}".format( + metadata[claim], policy[claim]["one_of"] + ) + ) + else: + # The following is for claims that can have lists of values + if "add" in policy[claim]: + metadata[claim] = list(union(metadata[claim], policy[claim]["add"])) + + if "subset_of" in policy[claim]: + _val = set(policy[claim]["subset_of"]).intersection( + set(metadata[claim]) + ) + if _val: + metadata[claim] = list(_val) + else: + raise PolicyError( + "{} not subset of {}".format( + metadata[claim], policy[claim]["subset_of"] + ) + ) + if "superset_of" in policy[claim]: + if set(policy[claim]["superset_of"]).difference( + set(metadata[claim]) + ): + raise PolicyError( + "{} not superset of {}".format( + metadata[claim], policy[claim]["superset_of"] + ) + ) + else: + pass + + # In policy but not in metadata + for claim in policy_set.difference(metadata_set): + if "value" in policy[claim]: + metadata[claim] = policy[claim]["value"] + elif "add" in policy[claim]: + metadata[claim] = policy[claim]["add"] + elif "default" in policy[claim]: + metadata[claim] = policy[claim]["default"] + + if claim not in metadata: + if "essential" in policy[claim] and policy[claim]["essential"]: + raise PolicyError("Essential claim '{}' missing".format(claim)) + + # All that are in metadata but not in policy should just remain + + return metadata + + +def diff2policy(new, old): + res = {} + for claim in set(new).intersection(set(old)): + if new[claim] == old[claim]: + continue + else: + res[claim] = {"value": new[claim]} + + for claim in set(new).difference(set(old)): + if claim in ["contacts"]: + res[claim] = {"add": new[claim]} + else: + res[claim] = {"value": new[claim]} + + return res diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/fa_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/fa_metadata.py new file mode 100644 index 0000000..fe8d22e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/fa_metadata.py @@ -0,0 +1,12 @@ +from typing import List + +from pydantic import BaseModel, HttpUrl, EmailStr + + +class FAMetadata(BaseModel): + contacts: List[EmailStr] + federation_fetch_endpoint: HttpUrl + federation_resolve_endpoint: HttpUrl + federation_list_endpoint: HttpUrl + homepage_uri: HttpUrl + organization_name: str diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/jwks.py new file mode 100644 index 0000000..23cc6d3 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/jwks.py @@ -0,0 +1,72 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel, validator + + +class Jwk(BaseModel): + kid: str # Base64url-encoded thumbprint string + kty: Literal["EC", "RSA"] + # TODO: verify if is optional + alg: Optional[ + Literal[ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + "PS256", + "PS384", + "PS512", + "RSA-OAEP", + "RSA-OAEP-256", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A256KW" + ] + ] + use: Optional[Literal["sig", "enc"]] + n: Optional[str] # Base64urlUInt-encoded + e: Optional[str] # Base64urlUInt-encoded + + def check_value_for_rsa(value, name, values): + if "EC" == values.get("kty") and value: + raise ValueError(f"{name} must be present only for kty = RSA") + + def check_value_for_ec(value, name, values): + if "RSA" == values.get("kty") and value: + raise ValueError(f"{name} must be present only for kty = EC") + + @validator("n") + def validate_n(cls, n_value, values): + cls.check_value_for_rsa(n_value, "n", values) + + @validator("e") + def validate_e(cls, e_value, values): + cls.check_value_for_rsa(e_value, "e", values) + + +class JwkCie(Jwk): + x: Optional[str] # Base64url-encoded + y: Optional[str] # Base64url-encoded + crv: Optional[Literal["P-256", "P-384", "P-521"]] + + @validator("x") + def validate_x(cls, x_value, values): + cls.check_value_for_ec(x_value, "x", values) + + @validator("y") + def validate_y(cls, y_value, values): + cls.check_value_for_ec(y_value, "y", values) + + @validator("crv") + def validate_crv(cls, crv_value, values): + cls.check_value_for_ec(crv_value, "crv", values) + + +class JwksCie(BaseModel): + keys: List[JwkCie] + + +class JwksSpid(BaseModel): + keys: List[Jwk] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/op_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/op_metadata.py new file mode 100644 index 0000000..e1db618 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/op_metadata.py @@ -0,0 +1,138 @@ +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel, HttpUrl, root_validator + +from .jwks import JwksCie + + +class ScopeSupported(str, Enum): + openid = "openid" + offline_access = "offline_access" + profile = "profile" + email = "email" + + +class ResponseModesSupported(str, Enum): + form_post = ("form_post",) + query = "query" + + +class GrantTypeSupported(str, Enum): + refresh_token = "refresh_token" # nosec - B105 + authorization_code = "authorization_code" + + +class AcrValuesSupported(str, Enum): + spid_l1 = "https://www.spid.gov.it/SpidL1" + spid_l2 = "https://www.spid.gov.it/SpidL2" + spid_l3 = "https://www.spid.gov.it/SpidL3" + + +class SigningAlgValuesSupported(str, Enum): + es256 = "ES256" + es384 = "ES384" + es512 = "ES512" + rs256 = "RS256" + rs384 = "RS384" + rs512 = "RS512" + # TODO: to be confirmed + # ps256 = "PS256" + # ps384 = "PS384" + # ps512 = "PS512" + + +class EncryptionAlgValuesSupported(str, Enum): + rsa_oaep = "RSA-OAEP" + ras_oaep_256 = "RSA-OAEP-256" + + +class EncryptionEncValuesSupported(str, Enum): + a128cbc_hs256 = "A128CBC-HS256" + a192cbc_hs384 = "A192CBC-HS384" + a256cbc_hs512 = "A256CBC-HS512" + a128gcm = "A128GCM" + a192gcm = "A192GCM" + a256gcm = "A256GCM" + rsa_oaep_256 = "RSA-OAEP-256" + + +class ClaimsSupported(str, Enum): + given_name = "given_name" + family_name = "family_name" + email = "email" + email_verified = "email_verified" + gender = "gender" + birthdate = "birthdate" + phone_number = "phone_number" + phone_number_verified = "phone_number_verified" + place_of_birth = "place_of_birth" + address = "address" + document_details = "document_details" + https_attributes_eid_gov_it_e_delivery_service = ( + "https://attributes.eid.gov.it/e_delivery_service" + ) + https_attributes_eid_gov_it_fiscal_number = ( + "https://attributes.eid.gov.it/fiscal_number" + ) + https_attributes_eid_gov_it_idANPR = "https://attributes.eid.gov.it/idANPR" + https_attributes_eid_gov_it_physical_phone_number = ( + "https://attributes.eid.gov.it/physical_phone_number" + ) + + +class OPMetadata(BaseModel): + organization_name: str + issuer: HttpUrl + authorization_endpoint: HttpUrl + token_endpoint: HttpUrl + userinfo_endpoint: HttpUrl + introspection_endpoint: HttpUrl + revocation_endpoint: HttpUrl + token_endpoint_auth_signing_alg_values_supported: List[SigningAlgValuesSupported] + id_token_encryption_alg_values_supported: List[EncryptionAlgValuesSupported] + id_token_encryption_enc_values_supported: List[EncryptionEncValuesSupported] + userinfo_encryption_alg_values_supported: List[EncryptionAlgValuesSupported] + userinfo_encryption_enc_values_supported: List[EncryptionEncValuesSupported] + request_object_encryption_alg_values_supported: List[EncryptionAlgValuesSupported] + request_object_encryption_enc_values_supported: List[EncryptionEncValuesSupported] + id_token_signing_alg_values_supported: List[SigningAlgValuesSupported] + userinfo_signing_alg_values_supported: List[SigningAlgValuesSupported] + request_object_signing_alg_values_supported: List[SigningAlgValuesSupported] + token_endpoint_auth_methods_supported = ["private_key_jwt"] + subject_types_supported = ["pairwise"] + request_parameter_supported = True + acr_values_supported: List[AcrValuesSupported] + signed_jwks_uri: Optional[HttpUrl] + jwks_uri: Optional[HttpUrl] + jwks: Optional[JwksCie] + + @root_validator(pre=False) + def validate(cls, values): + jwks_there = False + for i in ("jwks_uri", "jwks", "signed_jwks_uri"): + if values.get(i): + jwks_there = True + break + + if not jwks_there: + raise ValueError( + "one of signed_jwks_uri or jwks_uri or jwks must be set" + ) + return values + + +class OPMetadataCie(OPMetadata): + scopes_supported: List[ScopeSupported] + response_types_supported = ["code"] + response_modes_supported: List[ResponseModesSupported] + grant_types_supported: List[GrantTypeSupported] + claims_supported: List[ClaimsSupported] + claims_parameter_supported = True + tls_client_certificate_bound_access_tokens = True + authorization_response_iss_parameter_supported = True + + +class OPMetadataSpid(OPMetadata): + op_name: Optional[str] + op_uri: Optional[str] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/resolve_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/resolve_endpoint.py new file mode 100644 index 0000000..94d33b4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/resolve_endpoint.py @@ -0,0 +1,53 @@ + +from typing import List, Literal, Optional + +from pydantic import BaseModel, HttpUrl, constr + + +class ResolveResponse(BaseModel): + jose: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + + +class ResolveErrorResponse(BaseModel): + operation: Optional[str] + error: Literal[ + "invalid_request", + "invalid_subject", + "invalid_trust_anchor", + "invalid_trust_chain", + "invalid_metadata", + "not_found", + "server_error", + "temporarily_unavailable", + "unsupported_parameter", + ] + error_description: str + + +class ResolveRequest(BaseModel): + sub: HttpUrl + trust_anchor: HttpUrl + # OpenID Federation 8.3.1: entity_type OPTIONAL, may occur multiple times + entity_type: Optional[List[str]] = None + format: Optional[Literal["json", "jose"]] = None + + def example(): # pragma: no cover + return ResolveRequest( + sub="http://127.0.0.1:8000/oidc/rp", + trust_anchor="http://127.0.0.1:8000", + format="json", + ) + + +# class ResolveResponse(BaseModel): + # iss : HttpUrl + # sub : HttpUrl + # iat : int + # exp: int + # trust_marks : Json + # metadata: Json + # trust_chain: List + + +class ResolveHeader(BaseModel): + Authorization : Optional[str] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/rp_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/rp_metadata.py new file mode 100644 index 0000000..28e85cb --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/schemas/rp_metadata.py @@ -0,0 +1,46 @@ +from enum import Enum +from typing import List, Optional +from pydantic import BaseModel, HttpUrl, root_validator +from .jwks import JwksSpid + + +class GrantTypeSupported(str, Enum): + refresh_token = "refresh_token" # nosec - B105 + authorization_code = "authorization_code" + + +class RPMetadata(BaseModel): + organization_name: str + redirect_uris: List[HttpUrl] + response_types = ["code"] + grant_types: List[GrantTypeSupported] + client_id: HttpUrl + # TODO: Could be specified in multiple languages + client_name: str + + signed_jwks_uri: Optional[HttpUrl] + jwks_uri: Optional[HttpUrl] + jwks: Optional[JwksSpid] + + @root_validator(pre=False) + def validate(cls, values): + jwks_there = False + for i in ("jwks_uri", "jwks", "signed_jwks_uri"): + if values.get(i): + jwks_there = True + break + + if not jwks_there: + raise ValueError( + "one of signed_jwks_uri or jwks_uri or jwks must be set" + ) + return values + + +class RPMetadataSpid(RPMetadata): + pass + + +class RPMetadataCie(RPMetadataSpid): + application_type = "web" + tls_client_certificate_bound_access_tokens: Optional[bool] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/settings.py new file mode 100644 index 0000000..438c693 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/settings.py @@ -0,0 +1,93 @@ +from django.conf import settings + +import aiohttp + + +ENCRYPTION_ENC_SUPPORTED = [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM", +] + +DEFAULT_HASH_FUNC = "SHA-256" + +DEFAULT_JWS_ALG = getattr(settings, "DEFAULT_JWS_ALG", "RS256") +DEFAULT_JWE_ALG = getattr(settings, "DEFAULT_JWE_ALG", "RSA-OAEP") +DEFAULT_JWE_ENC = getattr(settings, "DEFAULT_JWE_ENC", "A256CBC-HS512") +SIGNING_ALG_VALUES_SUPPORTED = getattr( + settings, + "SIGNING_ALG_VALUES_SUPPORTED", + ["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"], +) +ENCRYPTION_ALG_VALUES_SUPPORTED = getattr( + settings, + "ENCRYPTION_ALG_VALUES_SUPPORTED", + [ + "RSA-OAEP", + "RSA-OAEP-256", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW", + ], +) + +# This is required in general project settings +# OIDCFED_TRUST_ANCHORS = [https://..., ] + +OIDCFED_ALLOWED_TRUST_MARKS = [] +OIDCFED_FILTER_BY_TRUST_MARKS = True + +# the metadata discovery will only processes the first MAXIMUM_AUTHORITY_HINTS +OIDCFED_MAX_PATH_LEN = 1 + +# old, for requests +# HTTPC_PARAMS = { +# "verify": True, +# "timeout": 4 +# } + +# for aiohttp +HTTPC_TIMEOUT = getattr(settings, "HTTPC_TIMEOUT", 12) +HTTPC_PARAMS = getattr( + settings, + "HTTPC_PARAMS", + { + "connection": {"ssl": True}, + "session": {"timeout": aiohttp.ClientTimeout(total=HTTPC_TIMEOUT)}, + }, +) + +# in minutes +MAX_ACCEPTED_TIMEDIFF = 5 + + +OIDCFED_MAXIMUM_AUTHORITY_HINTS = getattr( + settings, + "OIDCFED_MAXIMUM_AUTHORITY_HINTS", + 2, +) + +FEDERATION_DEFAULT_EXP = getattr(settings, "FEDERATION_DEFAULT_EXP", 2880) + +ENTITY_TYPE_LEAFS = getattr(settings, "ENTITY_TYPE_LEAFS", [ + "openid_relying_party", + "openid_provider", + "openid_credential_issuer", + "oauth_resource", + "wallet_provider", + "openid_credential_verifier" +]) + +ENTITY_TYPES = ["federation_entity"] + ENTITY_TYPE_LEAFS +ENTITY_STATUS = { + "unreachable": False, + "valid": True, + "signature_failed": False, + "not_valid": False, + "unknown": None, + "expired": None, +} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/statements.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/statements.py new file mode 100644 index 0000000..0ccc4d5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/statements.py @@ -0,0 +1,486 @@ +from copy import deepcopy +from .exceptions import ( + UnknownKid, + MissingJwksClaim, + MissingTrustMark, + TrustAnchorNeeded, +) +from .http_client import http_get +from .jwtse import verify_jws, unpad_jwt_head, unpad_jwt_payload + +import asyncio +import json +import logging +import requests + + +try: + from django.conf import settings +except ImportError: # pragma: no cover + from . import settings + + +OIDCFED_FEDERATION_WELLKNOWN_URL = ".well-known/openid-federation" +logger = logging.getLogger(__name__) + + +def jwks_from_jwks_uri(jwks_uri: str, httpc_params: dict = {}) -> list: + return [json.loads(asyncio.run(http_get([jwks_uri], httpc_params)))] # pragma: no cover + + +def get_federation_jwks(jwt_payload: dict, httpc_params: dict = {}): + return ( + jwt_payload.get("jwks", {}).get("keys", []) + ) + + +def get_http_url(urls: list, httpc_params: dict = {}) -> list: + if getattr(settings, "HTTP_CLIENT_SYNC", False): + responses = [] + for i in urls: + res = requests.get(i, **httpc_params) # nosec - B113 + responses.append(res.content.decode()) + else: + responses = asyncio.run(http_get(urls, httpc_params)) # pragma: no cover + return responses + + +def get_entity_statements(urls: list, httpc_params: dict = {}) -> list: + """ + Fetches an entity statement/configuration + """ + if isinstance(urls, str): + urls = [urls] # pragma: no cover + for url in urls: + logger.debug(f"Starting Entity Statement Request to {url}") + return get_http_url(urls, httpc_params) + + +def get_entity_configurations(subjects: list, httpc_params: dict = {}): + if isinstance(subjects, str): + subjects = [subjects] + urls = [] + for subject in subjects: + if subject[-1] != "/": + subject = f"{subject}/" + url = f"{subject}{OIDCFED_FEDERATION_WELLKNOWN_URL}" + urls.append(url) + logger.info(f"Starting Entity Configuration Request for {url}") + return get_http_url(urls, httpc_params) + + +def validate_entity_statement_header(header: dict) -> None: + """ + OpenID Federation 3.2: Entity Statements MUST have typ entity-statement+jwt + and kid (Key ID) in the header. Trust Marks are not validated here (typ can be customized). + """ + if header.get("typ") != "entity-statement+jwt": + raise ValueError( + f"Entity Statement must have typ=entity-statement+jwt, got {header.get('typ')}" + ) + kid = header.get("kid") + if not kid or not isinstance(kid, str) or not kid.strip(): + raise ValueError("Entity Statement must have non-empty kid header") + + +def get_trust_mark_type_id(payload_or_obj) -> str: + """ + Return trust mark type identifier from a Trust Mark JWT payload or from + a trust_marks array element (dict with trust_mark_type, trust_mark_id, or id). + Retrocompat: accepts trust_mark_type (draft 48), trust_mark_id, or id. + """ + if hasattr(payload_or_obj, "get"): + return ( + payload_or_obj.get("trust_mark_type") + or payload_or_obj.get("trust_mark_id") + or payload_or_obj.get("id") + ) + return None + + +class TrustMark: + def __init__(self, jwt: str, httpc_params: dict = {}): + self.jwt = jwt + self.header = unpad_jwt_head(jwt) + self.payload = unpad_jwt_payload(jwt) + + # Retrocompat: id from trust_mark_type (draft 48), trust_mark_id, or id + self.id = get_trust_mark_type_id(self.payload) + if not self.id: + raise KeyError( + "Trust Mark payload must contain one of: " + "trust_mark_type, trust_mark_id, id" + ) + self.sub = self.payload["sub"] + self.iss = self.payload["iss"] + + self.is_valid = False + + self.issuer_entity_configuration = None + self.httpc_params = httpc_params + + def validate_by(self, ec) -> bool: + # TODO: pydantic entity configuration validation here + + if self.header.get("kid") not in ec.kids: + raise UnknownKid( # pragma: no cover + f"Trust Mark validation failed: " + f"{self.header.get('kid')} not found in {ec.jwks}" + ) + # verify signature + payload = verify_jws(self.jwt, ec.jwks[ec.kids.index(self.header["kid"])]) + self.is_valid = True + return payload + + def validate_by_its_issuer(self) -> bool: + if not self.issuer_entity_configuration: + self.issuer_entity_configuration = get_entity_configurations( + self.iss, self.httpc_params + ) + try: + ec = EntityConfiguration(self.issuer_entity_configuration[0]) + ec.validate_by_itself() + except UnknownKid: + logger.warning( + f"Trust Mark validation failed by its Issuer: " + f"{self.header.get('kid')} not found in " + f"{self.issuer_entity_configuration.jwks}") + return False + except Exception: + logger.warning(f"Issuer {self.iss} of trust mark {self.id} is not valid.") + self.is_valid = False + return False + + # verify signature + payload = verify_jws( + self.jwt, + ec.jwks[ + ec.kids.index(self.header["kid"]) + ], + ) + self.is_valid = True + return payload + + def __repr__(self) -> str: + return f"{self.id} to {self.sub} issued by {self.iss}" + + +class EntityConfiguration: + """ + The self issued/signed statement of a federation entity + """ + + def __init__( + self, + jwt: str, + httpc_params: dict = {}, + filter_by_allowed_trust_marks: list = [], + trust_anchor_entity_conf=None, + trust_mark_issuers_entity_confs: dict = [], + ): + self.jwt = jwt + self.header = unpad_jwt_head(jwt) + validate_entity_statement_header(self.header) + self.payload = unpad_jwt_payload(jwt) + self.sub = self.payload["sub"] + self.iss = self.payload["iss"] + self.jwks = get_federation_jwks(self.payload, httpc_params) + if not self.jwks or not self.jwks[0]: + _msg = f"Missing jwks in the statement for {self.sub}" + logger.error(_msg) + raise MissingJwksClaim(_msg) + + self.kids = [i.get("kid") for i in self.jwks] + self.httpc_params = httpc_params + + self.filter_by_allowed_trust_marks = filter_by_allowed_trust_marks + self.trust_anchor_entity_conf = trust_anchor_entity_conf + self.trust_mark_issuers_entity_confs = trust_mark_issuers_entity_confs + + # a dict with sup_sub : superior entity configuration + self.verified_superiors = {} + # as previous but with superiors with invalid entity configurations + self.failed_superiors = {} + + # a dict with sup_sub : entity statement issued for self + self.verified_by_superiors = {} + self.failed_by_superiors = {} + + # a dict with the paylaod of valid entity statements for each descendant subject + self.verified_descendant_statements = {} + self.failed_descendant_statements = {} + + # a dict with the RAW JWT of valid entity statements for each descendant subject + self.verified_descendant_statements_as_jwt = {} + + self.verified_trust_marks = [] + self.is_valid = False + + def validate_by_itself(self) -> bool: + """ + validates the entity configuration by it self + """ + # TODO: pydantic entity configuration validation here + if self.header.get("kid") not in self.kids: + raise UnknownKid(f"{self.header.get('kid')} not found in {self.jwks}") # pragma: no cover + # verify signature + verify_jws(self.jwt, self.jwks[self.kids.index(self.header["kid"])]) + self.is_valid = True + return True + + def validate_by_allowed_trust_marks(self) -> bool: + """ + validate the entity configuration ony if marked by a well known + trust mark, issued by a trusted issuer + """ + + if not self.trust_anchor_entity_conf: + raise TrustAnchorNeeded( + "To validate the trust marks the " + "Trust Anchor Entity Configuration " + "is needed." + ) + + if not self.filter_by_allowed_trust_marks: + return True + + if not self.payload.get("trust_marks"): + logger.warning( + f"{self.sub} doesn't have the trust marks claim " + "in its Entity Configuration" + ) + return False + + trust_marks = [] + is_valid = False + for tm in self.payload["trust_marks"]: + # Retrocompat: accept trust_mark_type, trust_mark_id, or id + tm_type_id = get_trust_mark_type_id(tm) + if tm_type_id not in self.filter_by_allowed_trust_marks: + continue + + try: + trust_mark = TrustMark(tm["trust_mark"]) + except KeyError: + logger.warning( + f"Trust Mark decoding failed on [{tm}]. " + "Missing 'trust_mark' claim in it" + ) + except Exception: + logger.warning(f"Trust Mark decoding failed on [{tm}]") + continue + else: + trust_marks.append(trust_mark) + + if not trust_marks: + raise MissingTrustMark("Required Trust marks are missing.") # pragma: no cover + + trust_mark_issuers_by_id = self.trust_anchor_entity_conf.payload.get( + "trust_mark_issuers", {} + ) + + # TODO : cache of issuers -> it would be better to have a proxy function + # + # required_issuer_ecs = [] + # for trust_mark in trust_marks: + # if trust_mark.iss not in [ + # i.payload.get('iss', None) + # for i in self.trust_mark_issuers_entity_confs + # ]: + # required_issuer_ecs.append(trust_mark.iss) + # TODO: snippet for CACHE + # if required_issuer_ec: + # ## fetch the issuer entity configuration and validate it + # iecs = get_entity_configurations( + # [required_issuer_ecs], self.httpc_params + # ) + # for jwt in iecs: + # try: + # ec = self.__class__(jwt, httpc_params=self.httpc_params) + # ec.validate_by_itself() + # except Exception as e: + # logger.warning( + # "Trust Marks issuer Entity Configuration " + # f"failed for {jwt}: {e}" + # ) + # continue + # self.trust_mark_issuers_entity_confs.append(ec) + + for trust_mark in trust_marks: + id_issuers = trust_mark_issuers_by_id.get(trust_mark.id, None) + if id_issuers and trust_mark.iss not in id_issuers: + is_valid = False + elif id_issuers and trust_mark.iss in id_issuers: + is_valid = trust_mark.validate_by_its_issuer() + elif not id_issuers: + is_valid = trust_mark.validate_by(self.trust_anchor_entity_conf) + + if not trust_mark.is_valid: + is_valid = False + + if is_valid: + logger.info(f"Trust Mark {trust_mark} is valid") + self.verified_trust_marks.append(trust_mark) + else: + logger.warning(f"Trust Mark {trust_mark} is not valid") + + return is_valid + + def get_superiors( + self, + authority_hints: list = [], + max_authority_hints: int = 0, + superiors_hints: list = [], + ) -> dict: + """ + get superiors entity configurations + """ + # apply limits if defined + authority_hints = authority_hints or deepcopy(self.payload.get("authority_hints", [])) + if ( + max_authority_hints + and authority_hints != authority_hints[:max_authority_hints] + ): + logger.warning( + f"Found {len(authority_hints)} but " + f"authority maximum hints is set to {max_authority_hints}. " + "the following authorities will be ignored: " + f"{', '.join(authority_hints[max_authority_hints:])}" + ) + authority_hints = authority_hints[:max_authority_hints] + + for sup in superiors_hints: + if sup.sub in authority_hints: + logger.info( + "Getting Cached Entity Configurations for " + f"{[i.sub for i in superiors_hints]}" + ) + authority_hints.pop(authority_hints.index(sup.sub)) + self.verified_superiors[sup.sub] = sup + + logger.debug(f"Getting Entity Configurations for {authority_hints}") + + jwts = get_entity_configurations(authority_hints, self.httpc_params) + for jwt in jwts: + try: + ec = self.__class__(jwt, httpc_params=self.httpc_params) + except Exception as e: + logger.warning(f"Get Entity Configuration for {jwt}: {e}") + continue + + if ec.validate_by_itself(): + target = self.verified_superiors + else: + target = self.failed_superiors + + target[ec.payload["sub"]] = ec + + for ahints in authority_hints: + if not self.verified_superiors.get(ahints, None): + logger.warning( + f"{ahints} is not available, missing or not valid authority hint" + ) + continue + + return self.verified_superiors + + def validate_descendant_statement(self, jwt: str) -> bool: + """ + jwt is a descendant entity statement issued by self + """ + header = unpad_jwt_head(jwt) + validate_entity_statement_header(header) + payload = unpad_jwt_payload(jwt) + + if header.get("kid") not in self.kids: + raise UnknownKid(f"{self.header.get('kid')} not found in {self.jwks}") + # verify signature + payload = verify_jws(jwt, self.jwks[self.kids.index(header["kid"])]) + + self.verified_descendant_statements[payload["sub"]] = payload + self.verified_descendant_statements_as_jwt[payload["sub"]] = jwt + return self.verified_descendant_statements + + def validate_by_superior_statement(self, jwt: str, ec): + """ + jwt is a statement issued by a superior + ec is a superior entity configuration + + this method validates self with the jwks contained in statement + of the superior + """ + is_valid = None + payload = {} + try: + payload = unpad_jwt_payload(jwt) + ec.validate_by_itself() + ec.validate_descendant_statement(jwt) + _jwks = get_federation_jwks(payload, self.httpc_params) + _kids = [i.get("kid") for i in _jwks] + verify_jws(self.jwt, _jwks[_kids.index(self.header["kid"])]) + is_valid = True + except Exception as e: + logger.warning( + f"{self.sub} failed validation with " + f"{ec.sub}'s superior statement '{payload or jwt}'. " + f"Exception: {e}" + ) + is_valid = False + + if is_valid: + target = self.verified_by_superiors + ec.verified_descendant_statements[self.sub] = payload + ec.verified_descendant_statements_as_jwt[self.sub] = jwt + target[payload["iss"]] = ec + self.is_valid = True + return self.verified_by_superiors.get(ec.sub) + else: + target = self.failed_superiors + ec.failed_descendant_statements[self.sub] = payload + self.is_valid = False + + def validate_by_superiors( + self, + superiors_entity_configurations: dict = {}, + ): # -> dict[str, EntityConfiguration]: + """ + validates the entity configuration with the entity statements + issued by its superiors + + this methods create self.verified_superiors and failed ones + and self.verified_by_superiors and failed ones + """ + for ec in superiors_entity_configurations: + if ec.sub in ec.verified_by_superiors: + # already fetched and cached + continue + + try: + # get superior fetch url + fetch_api_url = ec.payload["metadata"]["federation_entity"][ + "federation_fetch_endpoint" + ] + except KeyError: + logger.warning( + "Missing federation_fetch_endpoint in " + f"federation_entity metadata for {self.sub} by {ec.sub}." + ) + self.failed_superiors[ec.sub] = None + continue + + else: + _url = f"{fetch_api_url}?sub={self.sub}" + logger.info(f"Getting entity statements from {_url}") + jwts = get_entity_statements([_url], self.httpc_params) + jwt = jwts[0] + if jwt: + self.validate_by_superior_statement(jwt, ec) + else: + logger.error( + f"Empty response for {_url}" + ) + + return self.verified_by_superiors + + def __repr__(self) -> str: + return f"{self.sub} valid {self.is_valid}" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/__init__.py new file mode 100644 index 0000000..31fdd64 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/__init__.py @@ -0,0 +1,8 @@ +from django.urls import reverse + + +def get_admin_change_view_url(obj: object) -> str: + return reverse( + "admin:{}_{}_change".format(obj._meta.app_label, type(obj).__name__.lower()), + args=(obj.pk,), + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/fa_metadata_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/fa_metadata_settings.py new file mode 100644 index 0000000..79caa48 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/fa_metadata_settings.py @@ -0,0 +1,9 @@ +FA_METADATA = { + "contacts": ["spid.tech@agid.gov.it"], + "federation_fetch_endpoint": "https://registry.spid.agid.gov.it/oidc/fedapi/", + "federation_list_endpoint": "https://registry.spid.agid.gov.it/oidc/list/", + "federation_trust_mark_status_endpoint": "https://registry.spid.agid.gov.it/oidc/status/", + "federation_resolve_endpoint": "https://registry.spid.agid.gov.it/oidc/resolve/", + "homepage_uri": "https://spid.agid.gov.it/", + "organization_name": "Tal dei tali" +} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/jwks_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/jwks_settings.py new file mode 100644 index 0000000..1aff080 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/jwks_settings.py @@ -0,0 +1,33 @@ +from copy import deepcopy + +JWKS = { + "keys": [ + { + "kty": "EC", + "kid": "sig-ec256-0", + "use": "sig", + "crv": "P-256", + "x": "2jM2df3IjB9VYQ0yz373-6EEot_1TBuTRaRYafMi5K0", + "y": "h6Zlz6XReK0L-iu4ZgxlozJEXgTGUFuuDl7o8b_8JnM", + }, + { + "kty": "EC", + "kid": "enc-ec256-1", + "use": "enc", + "crv": "P-256", + "x": "QI31cvWP4GwnWIi-Z0IYHauQ4nPCk8Vf1BHoPazGqEc", + "y": "DBwf8t9-abpXGtTDlZ8njjxAb33kOMrOqiGsd9oRxr0", + }, + ] +} + +JWKS_WITH_N_AND_EC_NO_CORRECT = deepcopy(JWKS) +JWKS_WITH_N_AND_EC_NO_CORRECT["keys"][0][ + "n" +] = "iu4ZgxlozJEXgTGUFuuDl7o8b_8JnMswwddaQjhFkKk" + +JWKS_WITH_X_AND_RSA_NO_CORRECT = deepcopy(JWKS) +JWKS_WITH_X_AND_RSA_NO_CORRECT["keys"][0][ + "x" +] = "iu4ZgxlozJEXgTGUFuuDl7o8b_8JnMswwddaQjhFkKk" +JWKS_WITH_X_AND_RSA_NO_CORRECT["keys"][0]["kty"] = "RSA" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/op_metadata_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/op_metadata_settings.py new file mode 100644 index 0000000..335f970 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/op_metadata_settings.py @@ -0,0 +1,91 @@ +from copy import deepcopy + +from spid_cie_oidc.entity.tests.jwks_settings import JWKS, JWKS_WITH_N_AND_EC_NO_CORRECT + +OP_METADATA = { + "issuer": "https://idserver.servizicie.interno.gov.it/op/", + "authorization_endpoint": "https://idserver.servizicie.interno.gov.it/op/auth", + "token_endpoint": "https://idserver.servizicie.interno.gov.it/op/token", + "userinfo_endpoint": "https://idserver.servizicie.interno.gov.it/op/userinfo", + "introspection_endpoint": "https://idserver.servizicie.interno.gov.it/op/introspect", + "revocation_endpoint": "https://idserver.servizicie.interno.gov.it/op/revoke", + "jwks_uri": "https://idserver.servizicie.interno.gov.it/.well-known/jwks.json", + "subject_types_supported": ["pairwise"], + "token_endpoint_auth_signing_alg_values_supported" : ["ES256"], + "id_token_encryption_alg_values_supported": ["RSA-OAEP"], + "request_object_encryption_enc_values_supported": ["A256CBC-HS512"], + "token_endpoint_auth_methods_supported": ["private_key_jwt"], + "userinfo_encryption_alg_values_supported": ["RSA-OAEP"], + "id_token_encryption_enc_values_supported": ["A256CBC-HS512"], + "id_token_signing_alg_values_supported": ["ES256"], + "userinfo_signing_alg_values_supported": ["ES256"], + "request_object_encryption_alg_values_supported": ["RSA-OAEP"], + "request_object_signing_alg_values_supported": ["ES256"], + "userinfo_encryption_enc_values_supported": ["A256CBC-HS512"], + "request_parameter_supported": True, + "subject_types_supported": ["pairwise"], + "token_endpoint_auth_methods_supported": ["private_key_jwt"], + "op_name": "Agenzia per l’Italia Digitale", + "op_uri": "https://www.agid.gov.it", + "acr_values_supported":[ + "https://www.spid.gov.it/SpidL1", + "https://www.spid.gov.it/SpidL2", + "https://www.spid.gov.it/SpidL3" + ], + "organization_name": "Tal dei tali" +} + +OP_METADATA_CIE = deepcopy(OP_METADATA) +OP_METADATA_CIE["scopes_supported"] = ["openid", "offline_access", "email"] +OP_METADATA_CIE["response_types_supported"] = ["code"] +OP_METADATA_CIE["response_modes_supported"] = ["query", "form_post"] +OP_METADATA_CIE["grant_types_supported"] = ["authorization_code", "refresh_token"] +OP_METADATA_CIE["claims_supported"] = [ + "given_name", + "family_name", + "email", + "email_verified", + "gender", + "birthdate", + "phone_number", + "phone_number_verified", + "place_of_birth", + "address", + "document_details", + "https://attributes.eid.gov.it/e_delivery_service", + "https://attributes.eid.gov.it/fiscal_number", + "https://attributes.eid.gov.it/idANPR", + "https://attributes.eid.gov.it/physical_phone_number", +] +OP_METADATA_CIE["claims_parameter_supported"] = True +OP_METADATA_CIE["tls_client_certificate_bound_access_tokens"] = True +OP_METADATA_CIE["authorization_response_iss_parameter_supported"] = True +OP_METADATA_CIE["userinfo_signing_alg_values_supported"] = ["ES256"] + + +OP_METADATA_CIE_JWKS_AND_JWKS_URI = deepcopy(OP_METADATA_CIE) +OP_METADATA_CIE_JWKS_AND_JWKS_URI["jwks"] = JWKS + +OP_METADATA_CIE_JWKS_URI_NO_CORRECT = deepcopy(OP_METADATA_CIE) +OP_METADATA_CIE_JWKS_URI_NO_CORRECT[ + "jwks_uri" +] = "https://registry.cie.gov.it/.well-known/" + +OP_METADATA_SPID = deepcopy(OP_METADATA) +OP_METADATA_SPID["op_name"] = "Agenzia per l’Italia Digitale" +OP_METADATA_SPID["op_uri"] = "https://www.agid.gov.it" +OP_METADATA_SPID["userinfo_signing_alg_values_supported"] = ["RS384"] +OP_METADATA_SPID["acr_values_supported"] = [ + "https://www.spid.gov.it/SpidL1", + "https://www.spid.gov.it/SpidL2", + "https://www.spid.gov.it/SpidL3", +] + +OP_METADATA_SPID_JWKS_AND_JWKS_URI = deepcopy(OP_METADATA_SPID) +OP_METADATA_SPID_JWKS_AND_JWKS_URI["jwks"] = JWKS + +OP_METADATA_SPID_JWKS_NO_JWKS_URI = deepcopy(OP_METADATA_SPID_JWKS_AND_JWKS_URI) +OP_METADATA_SPID_JWKS_NO_JWKS_URI.pop("jwks_uri") + +OP_METADATA_SPID_JWKS_EC_NO_CORRECT = deepcopy(OP_METADATA_SPID_JWKS_NO_JWKS_URI) +OP_METADATA_SPID_JWKS_EC_NO_CORRECT["jwks"] = JWKS_WITH_N_AND_EC_NO_CORRECT diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/rp_metadata_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/rp_metadata_settings.py new file mode 100644 index 0000000..4653c02 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/rp_metadata_settings.py @@ -0,0 +1,97 @@ +from copy import deepcopy + +from spid_cie_oidc.entity.tests.jwks_settings import JWKS + +RP_METADATA = { + "redirect_uris": [ + "https://rp.example.it/spid/callback1/", + "https://rp.example.it/spid/callback2/", + ], + "jwks_uri": "https://registry.cie.gov.it/keys.json", + "response_types": ["code"], + "grant_types": ["authorization_code", "refresh_token"], + "client_id": "https://rp.example.it/spid", + "client_name": "Nome RP", + "contacts": ["ops@rp.example.it"], + "subject_type": "pairwise", + "logo_uri": [ + "https://rp.example.it/logo_small.jpg", + "https://rp.example.it/logo_big.jpg" + ], + "scope" : ["openid", "offline_access", "profile"], + "organization_name": "Tal dei tali" +} + +RP_METADATA_CIE = deepcopy(RP_METADATA) +RP_METADATA_CIE["tls_client_certificate_bound_access_tokens"] = True +RP_METADATA_CIE["application_type"] = "web" + +RP_METADATA_SPID = deepcopy(RP_METADATA) + +RP_METADATA_SPID_NO_REDIRECT_URIS = deepcopy(RP_METADATA_SPID) +RP_METADATA_SPID_NO_REDIRECT_URIS.pop("redirect_uris") + + +RP_METADATA_SPID_JWKS_AND_JWKS_URI = deepcopy(RP_METADATA_SPID) +RP_METADATA_SPID_JWKS_AND_JWKS_URI["jwks"] = JWKS + +RP_METADATA_CIE_JWKS_AND_JWKS_URI = deepcopy(RP_METADATA_CIE) +RP_METADATA_CIE_JWKS_AND_JWKS_URI["jwks"] = JWKS + +RP_METADATA_CIE_NO_REDIRECT_URIS = deepcopy(RP_METADATA_CIE) +RP_METADATA_CIE_NO_REDIRECT_URIS.pop("redirect_uris") + +RP_METADATA_SPID_NOJWKS_NOJWKS_URI = deepcopy(RP_METADATA_SPID) +RP_METADATA_SPID_NOJWKS_NOJWKS_URI.pop("jwks_uri") + + +RP_METADATA_CIE_NOJWKS_NOJWKS_URI = deepcopy(RP_METADATA_CIE) +RP_METADATA_CIE_NOJWKS_NOJWKS_URI.pop("jwks_uri") + + +RP_METADATA_JWK1 = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} +RP_METADATA_JWK1_pub = {'use': 'sig', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} + +RP_METADATA_JWK2 = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} +RP_METADATA_JWK2_pub = {'alg': 'RSA-OAEP', 'use': 'enc', 'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'ENC-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} + +rp_onboarding_data = dict( + name="RP Test", + sub="http://rp-test.it/oidc/rp/", + type="openid_relying_party", + metadata_policy={"openid_relying_party": {"scope": {"value": ["openid"]}}}, + is_active=True, + jwks = [RP_METADATA_JWK1_pub] +) + +rp_conf = { + "sub": rp_onboarding_data["sub"], + "jwks_fed" : [RP_METADATA_JWK1], + "jwks_core" : [RP_METADATA_JWK1, RP_METADATA_JWK2], + "metadata": { + "openid_relying_party": { + "application_type": "web", + "client_registration_types": ["automatic"], + "client_name": "Name of this service called http://rp-test.it/oidc/rp/", + "contacts": ["ops@rp.example.it"], + "grant_types": ["refresh_token", "authorization_code"], + "redirect_uris": ["http://rp-test.it/oidc/rp/callback/"], + "response_types": ["code"], + "subject_type": "pairwise", + "client_id": "http://rp-test.it/oidc/rp/", + "jwks": {"keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub]}, + } + }, + "authority_hints": ["http://testserver/"], + "is_active": True, +} + +RP_CONF_AS_JSON = { + "iss": rp_conf["sub"], + "sub": rp_conf["sub"], + "jwks": { + "keys": [RP_METADATA_JWK1_pub, RP_METADATA_JWK2_pub] + }, + "metadata": rp_conf["metadata"], + "authority_hints":rp_conf["authority_hints"] +} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/settings.py new file mode 100644 index 0000000..77c06f3 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/settings.py @@ -0,0 +1,44 @@ +TA_SUB = "http://testserver/" +FA_METADATA = { + "federation_entity": { + "contacts": ["ops@localhost"], + "federation_fetch_endpoint": f"{TA_SUB}/fetch", + "federation_list_endpoint": f"{TA_SUB}/list/", + "homepage_uri": f"{TA_SUB}", + "name": "example TA", + } +} +TM_ISSUERS = { + "https://www.spid.gov.it/certification/rp/public": [ + TA_SUB, + "https://public.intermediary.spid.it", + ], + "https://www.spid.gov.it/certification/rp/private": [ + TA_SUB, + "https://private.other.intermediary.it", + ], + "https://sgd.aa.it/onboarding": ["https://sgd.aa.it"], +} +FA_CONSTRAINTS = {"max_path_length": 1} + +ta_conf_data = dict( + sub=TA_SUB, + metadata=FA_METADATA, + constraints=FA_CONSTRAINTS, + is_active=1, + trust_mark_issuers=TM_ISSUERS, +) +TA_JWK_PRIVATE = {'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'd': 'jEDxjcTZXBbgBV8Bgt7-qfW1FJoHDEFKFxhfMpHQQoETa-jTPhCxOD2MzYM8A-9kKc8tu9r-crTAl1PI42kPnMd283phixd5G5Tv8gSaGdnq-45ka0iRuC7TItUdDiMNb_2YzB4ZLGLNmaIKQJSGqCHEcQuRVyxJtTZwrXaMMOhDqJaWUvUQWF5C7g5O5mOVTkNKw6ujzhqcWa4N3NE-HwcbVW_9st4s1c_ng-DlwLTptaeM5j-LOeZMX1zcVlwYMi5ZkYYY6FHHjYI4nBWDtqhvf-64QaTv8exIjk8PcxHOwhfLTWiHPLk14af7U_pCzkP87WQCBgNfvt3WILQ5DQ', 'p': '75eNHkWaYQMgzVfFwif5uftSxqOhFU6VkxNKdqoRuFxJuVTO-M-vbQc3BwPxms2xrpizU6zGcoPGPvccDi0G040wZh34pWDVABMgGMKXKmeTwj8FuM1DzOVq8DKHmdrhk1gaQbPAP8JVOVYK7uh_lG5wmz3X-En1McMk-E8g8Ic', 'q': '0Sny6DLNtDP1_B9qiyCaMtRqPSAUZ1ohCZRlBT6-IGRR31Kt5S2JcVNDnF5w4dunlDY4nhIBZ0v0VyzWKgDXj6qrFY1pm1iE29gW227YsVRWQU8xWGpBwEu8nxNMr0u0zfe0QEGWU4RvNAsZPRa31HU87Vm7I3NSZ34DZsCZJoc', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} +TA_JWK_PUBLIC = {'kty': 'RSA', 'n': 'w8H80eT2zrs2XQ-SApZG9TkuXDuIxANfCVHt4fFqNnOEZaCNWqlTQIo0JiSBE-QmzZ09TYP1BJpESuQf_PUeLRVPfYHsBVk5OYvhT27_nYlV7_1LsFGLxxsIa-hswMMzvW-1_huKLy6Fp0WP0ouUJAHsF_eYVtO1ApRhvlIVd5azM4k7t8Lh8lkCSdF1SfGHfXnXJRb-XensZ0cFSfe2Koq9mD7jpGLXlPpXxj8Ow0g7KYT5kVtWE5ULmNmO7BIN1Hx4HpggbbEGgC9FyjKw4GfFb-csnB-icBPf_60HomjrkFFt6vTjrcqQaHOj-sEjP36N8rMSBiMmiMSPnsHhMQ', 'e': 'AQAB', 'kid': 'HIvo33-Km7n03ZqKDJfWVnlFudsW28YhQZx5eaXtAKA'} + +ta_conf_data_as_json = dict( + sub=TA_SUB, + iss=TA_SUB, + metadata=FA_METADATA, + jwks ={ + "keys": [TA_JWK_PUBLIC] + }, + constraints=FA_CONSTRAINTS, + is_active=1, + trust_mark_issuers=TM_ISSUERS, +) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_entity_configuration.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_entity_configuration.py new file mode 100644 index 0000000..057b243 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_entity_configuration.py @@ -0,0 +1,55 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase, Client +from django.urls import reverse + +from spid_cie_oidc.entity.models import * +from spid_cie_oidc.entity.jwtse import verify_jws + +from . import get_admin_change_view_url +from .settings import * + + +class EntityConfigurationTest(TestCase): + def setUp(self): + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + self.admin_user = get_user_model().objects.create_superuser( + username="superuser", password="secret", email="admin@example.com" + ) + + def test_entity_configuration_admin(self): + c = Client() + c.login(username="superuser", password="secret") + + # run test + response = c.get(get_admin_change_view_url(self.ta_conf)) + self.assertEqual(response.status_code, 200) + + def test_entity_configuration(self): + self.assertTrue( + isinstance( + FederationEntityConfiguration.get_active_conf(), + FederationEntityConfiguration, + ) + ) + self.assertTrue(isinstance(self.ta_conf, FederationEntityConfiguration)) + self.assertFalse(self.ta_conf.is_leaf) + + for i in ( + "public_jwks", + "pems_as_json", + "kids", + "type", + "entity_configuration_as_jws", + ): + attr = getattr(self.ta_conf, i) + self.assertTrue(attr) + + md = self.ta_conf.entity_configuration_as_dict["metadata"]["federation_entity"] + for i in ("federation_fetch_endpoint",): + self.assertTrue(md.get(i)) + + # dulcis in fundo -> test the .well-knwon/openid-federation + wk_url = reverse("entity_configuration") + c = Client() + res = c.get(wk_url) + verify_jws(res.content.decode(), self.ta_conf.jwks_fed[0]) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_op_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_op_metadata.py new file mode 100644 index 0000000..cabf2ad --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_01_op_metadata.py @@ -0,0 +1,37 @@ +import logging + +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.entity.schemas.op_metadata import OPMetadataCie, OPMetadataSpid +from spid_cie_oidc.entity.tests.op_metadata_settings import ( + OP_METADATA_CIE, + OP_METADATA_CIE_JWKS_AND_JWKS_URI, + OP_METADATA_SPID, + OP_METADATA_SPID_JWKS_AND_JWKS_URI, + OP_METADATA_SPID_JWKS_EC_NO_CORRECT, + OP_METADATA_SPID_JWKS_NO_JWKS_URI, +) + +logger = logging.getLogger(__name__) + + +class OpMetadataTest(TestCase): + def test_op_metatada_cie(self): + OPMetadataCie(**OP_METADATA_CIE) + + def test_op_metatada_cie_jwks_and_jwks_uri(self): + self.assertTrue(OPMetadataCie(**OP_METADATA_CIE_JWKS_AND_JWKS_URI)) + + def test_op_metatada_spid(self): + OPMetadataSpid(**OP_METADATA_SPID) + + def test_op_metatada_spid_jwks_and_jwks_uri(self): + self.assertTrue(OPMetadataSpid(**OP_METADATA_SPID_JWKS_AND_JWKS_URI)) + + def test_op_metatada_spid_jwks_no_jwks_uri(self): + OPMetadataSpid(**OP_METADATA_SPID_JWKS_NO_JWKS_URI) + + def test_op_metatada_spid_jws_ec_no_correct(self): + with self.assertRaises(ValidationError): + OPMetadataSpid(**OP_METADATA_SPID_JWKS_EC_NO_CORRECT) + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_02_rp_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_02_rp_metadata.py new file mode 100644 index 0000000..2c8e279 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_02_rp_metadata.py @@ -0,0 +1,43 @@ +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.entity.schemas.rp_metadata import RPMetadataCie, RPMetadataSpid +from spid_cie_oidc.entity.tests.rp_metadata_settings import ( + RP_METADATA_CIE, + RP_METADATA_SPID, + RP_METADATA_SPID_JWKS_AND_JWKS_URI, + RP_METADATA_SPID_NO_REDIRECT_URIS, + RP_METADATA_CIE_JWKS_AND_JWKS_URI, + RP_METADATA_CIE_NO_REDIRECT_URIS, + RP_METADATA_SPID_NOJWKS_NOJWKS_URI, + RP_METADATA_CIE_NOJWKS_NOJWKS_URI +) + + +class RPMetadataTest(TestCase): + def test_rp_metadataCie(self): + RPMetadataCie(**RP_METADATA_CIE) + + def test_rp_metadataSpid(self): + RPMetadataSpid(**RP_METADATA_SPID) + + def test_rp_metadataSpid_no_redirect_uris(self): + with self.assertRaises(ValidationError): + RPMetadataSpid(**RP_METADATA_SPID_NO_REDIRECT_URIS) + + def test_rp_metadataSpid_jwks_and_jwks_uri(self): + self.assertTrue(RPMetadataSpid(**RP_METADATA_SPID_JWKS_AND_JWKS_URI)) + + def test_rp_metadataSpid_no_jwks_and_no_jwks_uri(self): + with self.assertRaises(ValidationError): + RPMetadataSpid(**RP_METADATA_SPID_NOJWKS_NOJWKS_URI) + + def test_rp_metadataCie_jwks_and_jwks_uri(self): + self.assertTrue(RPMetadataCie(**RP_METADATA_CIE_JWKS_AND_JWKS_URI)) + + def test_rp_metadataCie_no_redirect_uris(self): + with self.assertRaises(ValidationError): + RPMetadataCie(**RP_METADATA_CIE_NO_REDIRECT_URIS) + + def test_rp_metadataCie_no_jwks_and_no_jwks_uri(self): + with self.assertRaises(ValidationError): + RPMetadataCie(**RP_METADATA_CIE_NOJWKS_NOJWKS_URI) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_03_jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_03_jwks.py new file mode 100644 index 0000000..8c35f88 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_03_jwks.py @@ -0,0 +1,24 @@ +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.entity.schemas.jwks import JwksCie, JwksSpid +from spid_cie_oidc.entity.tests.jwks_settings import ( + JWKS, + JWKS_WITH_N_AND_EC_NO_CORRECT, + JWKS_WITH_X_AND_RSA_NO_CORRECT, +) + + +class JwksTest(TestCase): + def test_jwks_spid(self): + JwksSpid(**JWKS) + + def test_jwks_cie(self): + JwksCie(**JWKS) + + def test_jwks_with_n_and_ec_no_correct(self): + with self.assertRaises(ValidationError): + JwksSpid(**JWKS_WITH_N_AND_EC_NO_CORRECT) + + def test_jwks_with_e_and_rsa_no_correct(self): + with self.assertRaises(ValidationError): + JwksCie(**JWKS_WITH_X_AND_RSA_NO_CORRECT) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_04_fa_metadata.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_04_fa_metadata.py new file mode 100644 index 0000000..1d5f9cd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_04_fa_metadata.py @@ -0,0 +1,8 @@ +from django.test import TestCase +from spid_cie_oidc.entity.schemas.fa_metadata import FAMetadata +from spid_cie_oidc.entity.tests.fa_metadata_settings import FA_METADATA + + +class FAMetadatTest(TestCase): + def test_fa_metadata(self): + FAMetadata(**FA_METADATA) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_05_policy.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_05_policy.py new file mode 100644 index 0000000..02ae7f9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_05_policy.py @@ -0,0 +1,91 @@ +from copy import deepcopy + +from django.test import TestCase +from spid_cie_oidc.entity.policy import ( + apply_policy, + diff2policy, + gather_policies, + PolicyError +) +from spid_cie_oidc.entity.tests.rp_metadata_settings import RP_METADATA +from spid_cie_oidc.authority.tests.settings import rp_onboarding_data + + +class PolicyTest(TestCase): + def setUp(self): + pass + + def test_gather_polices(self): + combined_policy = gather_policies([{}], "openid_relying_party") + self.assertTrue(combined_policy == {}) + + def test_gather_polices_rp(self): + combined_policy = gather_policies([{},rp_onboarding_data], "openid_relying_party") + self.assertTrue(combined_policy == {'scope': {'value': ['openid']}}) + + def test_apply_policy(self): + fa_policy = {} + fa_policy["contacts"] = {"add": "ciao@email.it"} + combined_policy = apply_policy(deepcopy(RP_METADATA), fa_policy) + combined_contacts = combined_policy["contacts"] + self.assertTrue("ciao@email.it" in combined_contacts) + self.assertTrue("ops@rp.example.it" in combined_contacts) + + def test_apply_policy_one_of(self): + fa_policy = {} + fa_policy["logo_uri"] = {"one_of": ["logo1", "logo2"]} + with self.assertRaises(PolicyError): + apply_policy(deepcopy(RP_METADATA), fa_policy) + + def test_apply_policy_subset_of(self): + fa_policy = {} + fa_policy["scope"] = {"subset_of": ["openid", "offline_access"]} + combined_contacts = apply_policy(deepcopy(RP_METADATA), fa_policy) + self.assertTrue('profile' not in combined_contacts["scope"]) + + def test_apply_policy_superset_of(self): + fa_policy = {} + fa_policy["scope"] = {"superset_of": ["openid"]} + RP_METADATA_LOCAL = deepcopy(RP_METADATA) + RP_METADATA_LOCAL["scope"] = ["offline_access"] + with self.assertRaises(PolicyError): + apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy) + + def test_apply_policy_difference_value(self): + fa_policy = {} + fa_policy["client_id"] = {"value": "https://rp.example.it/spid"} + RP_METADATA_LOCAL = deepcopy(RP_METADATA) + RP_METADATA_LOCAL.pop("client_id") + combined_contacts = apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy) + self.assertTrue(combined_contacts["client_id"]) + + def test_apply_policy_difference_add(self): + fa_policy = {} + fa_policy["contacts"] = {"add": "ciao@email.it"} + RP_METADATA_LOCAL = deepcopy(RP_METADATA) + RP_METADATA_LOCAL.pop("contacts") + combined_contacts = apply_policy(deepcopy(RP_METADATA_LOCAL), fa_policy) + self.assertTrue(combined_contacts["contacts"]) + self.assertTrue("ciao@email.it" in combined_contacts["contacts"]) + + def test_apply_policy_add(self): + fa_policy = {} + fa_policy["contacts"] = {"add": "ciao@email.it"} + combined_contacts = apply_policy(deepcopy(RP_METADATA), fa_policy) + self.assertTrue("ciao@email.it" in combined_contacts["contacts"]) + self.assertTrue("ops@rp.example.it" in combined_contacts["contacts"]) + + + def test_diff_two_policy(self): + fa_policy_old = {} + fa_policy_new = deepcopy(fa_policy_old) + fa_policy_new["contacts"] = {"add": "test@email.it"} + result = diff2policy(fa_policy_new, fa_policy_old) + diff = { + "contacts": { + "add": { + "add": "test@email.it" + } + } + } + self.assertTrue(result == diff) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_06_http_client.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_06_http_client.py new file mode 100644 index 0000000..23541b1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_06_http_client.py @@ -0,0 +1,12 @@ +import asyncio + +from django.test import TestCase +from spid_cie_oidc.entity.http_client import http_get + + +class HttpClient(TestCase): + def test_http_client(self): + url = "https://icanhazip.com/" + get = http_get([url], {}) + value = asyncio.run(get) + self.assertFalse(len(value) == 0) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_resolve_entity_statement.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_resolve_entity_statement.py new file mode 100644 index 0000000..820f4eb --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_resolve_entity_statement.py @@ -0,0 +1,96 @@ +import json + +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import ( + Client, + RequestFactory, + TestCase, + override_settings +) +from django.urls import reverse +from spid_cie_oidc.entity.tests.rp_metadata_settings import rp_conf +from spid_cie_oidc.entity.views import resolve_entity_statement +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain, + StaffToken +) +from spid_cie_oidc.entity.tests.settings import ta_conf_data +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) + + +def create_tc(): + + + TA_FES = FetchedEntityStatement.objects.create( + sub="http://testserver/", + iss="http://testserver/", + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + return TrustChain.objects.create( + sub="http://rp-test.it/oidc/rp/", + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=[], + status="valid", + trust_anchor=TA_FES, + is_active=True, + ) + +class ResolveEntityStatementTest(TestCase): + + def setUp(self): + self.factory = RequestFactory() + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + is_staff = True + ) + self.user.set_password("test") + self.user.save() + StaffToken.objects.create( + user = self.user, + token = "secret-token", + is_active = True + ) + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + self.sub_conf = FederationEntityConfiguration.objects.create(**rp_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=ta_conf_data["sub"], + iss=ta_conf_data["sub"], + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + + + + @override_settings(HTTP_CLIENT_SYNC=True) + def test_resolve_entity_statement(self): + client = Client() + url = reverse("oidcfed_resolve") + client.login(username="test", password="test") + data = {"sub" : rp_conf["sub"], "trust_anchor" : ta_conf_data["sub"]} + request = self.factory.get(url, data, **{'HTTP_AUTHORIZATION': "secret-token"}) + self.patcher = patch( + "spid_cie_oidc.entity.trust_chain_operations.get_or_create_trust_chain", + return_value = create_tc() + ) + self.patcher.start() + res = resolve_entity_statement(request, format='json') + self.patcher.stop() + _json = json.loads(res.content.decode()) + self.assertTrue(_json.get("iss") == ta_conf_data["sub"]) + self.assertTrue(_json.get("sub") == rp_conf["sub"]) + + self.assertTrue("trust_chain" in _json) + self.assertTrue("metadata" in _json) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_validators.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_validators.py new file mode 100644 index 0000000..a2e2eaf --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_07_validators.py @@ -0,0 +1,81 @@ +from copy import deepcopy + +from django.core.exceptions import ValidationError +from django.test import TestCase +from spid_cie_oidc.authority.tests.settings import ( + RP_METADATA_JWK1, + RP_METADATA_JWK1_pub +) +from spid_cie_oidc.entity.tests.op_metadata_settings import OP_METADATA +from spid_cie_oidc.entity.tests.rp_metadata_settings import RP_METADATA_SPID +from spid_cie_oidc.entity.validators import ( + validate_entity_metadata, + validate_metadata_algs, + validate_private_jwks, + validate_public_jwks +) + + +class ValidatrTest(TestCase): + + def setUp(self): + pass + + def test_validate_public_jwks_with_private_key(self): + with self.assertRaises(Exception): + validate_public_jwks(RP_METADATA_JWK1) + + def test_validate_private_jwks_with_public_key(self): + with self.assertRaises(Exception): + validate_private_jwks(RP_METADATA_JWK1_pub) + + def test_validate_public_jwks(self): + validate_public_jwks(RP_METADATA_JWK1_pub) + + def test_validate_private_jwks(self): + validate_private_jwks(RP_METADATA_JWK1) + + def test_validate_metadata_algs_ok(self): + op_md = {} + op_md["openid_provider"] = OP_METADATA + validate_metadata_algs(op_md) + + def test_validate_metadata_algs_with_error(self): + OP_METADATA_NO_CORRECT = deepcopy(OP_METADATA) + OP_METADATA_NO_CORRECT["token_endpoint_auth_signing_alg_values_supported"] = ["ciao"] + op_md = {} + op_md["openid_provider"] = OP_METADATA_NO_CORRECT + with self.assertRaises(ValidationError): + validate_metadata_algs(op_md) + + def test_validate_entity_op_metadata(self): + op_md = {} + op_md["openid_provider"] = deepcopy(OP_METADATA) + validate_entity_metadata(op_md) + + def test_validate_entity_no_valid_op_metadata(self): + op_md = {} + op_md["openid_provider"] = deepcopy(OP_METADATA) + op_md["openid_provider"].pop("issuer") + with self.assertRaises(ValidationError): + validate_entity_metadata(op_md) + + def test_validate_entity_rp_metadata(self): + rp_md = {} + rp_md["openid_relying_party"] = deepcopy(RP_METADATA_SPID) + validate_entity_metadata(rp_md) + + def test_validate_entity_no_valid_rp_metadata(self): + rp_md = {} + rp_md["openid_relying_party"] = deepcopy(RP_METADATA_SPID) + rp_md["openid_relying_party"].pop("grant_types") + with self.assertRaises(ValidationError): + validate_entity_metadata(rp_md) + + def test_validate_entity_no_entity_type(self): + rp_md = {} + rp_md = deepcopy(RP_METADATA_SPID) + with self.assertRaises(ValidationError): + validate_entity_metadata(rp_md) + + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_08_statements.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_08_statements.py new file mode 100644 index 0000000..88b512c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_08_statements.py @@ -0,0 +1,40 @@ + +from unittest.mock import patch +from django.test import TestCase, override_settings +from spid_cie_oidc.authority.models import FederationDescendant, FederationEntityAssignedProfile, FederationEntityProfile + +from spid_cie_oidc.authority.tests.mocked_responses import EntityResponseNoIntermediate +from spid_cie_oidc.entity.models import FederationEntityConfiguration + +from spid_cie_oidc.entity.tests.settings import ta_conf_data +from spid_cie_oidc.authority.tests.settings import ( + rp_conf, + RP_PROFILE, + rp_onboarding_data +) +from spid_cie_oidc.entity.statements import TrustMark + +class StatementTest(TestCase): + + def setUp(self): + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf) + + self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE) + self.rp = FederationDescendant.objects.create(**rp_onboarding_data) + + self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create( + descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf + ) + self.trust_mark = self.rp_assigned_profile.trust_mark_as_jws + + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=EntityResponseNoIntermediate()) + def test_validate_by_its_issuer(self, mocked): + self.tm = TrustMark(self.trust_mark) + TrustMark.validate_by_its_issuer(self.tm) + self.assertTrue(self.tm.is_valid) + + + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_09_trust_chain.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_09_trust_chain.py new file mode 100644 index 0000000..e0537d2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_09_trust_chain.py @@ -0,0 +1,59 @@ + +from unittest.mock import patch + +from django.test import TestCase + +from spid_cie_oidc.authority.tests.settings import ( + RP_CONF_AS_JSON, + RP_METADATA_JWK1, + rp_conf +) +from spid_cie_oidc.entity.exceptions import InvalidEntityConfiguration +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.tests.settings import ( + TA_JWK_PRIVATE, + ta_conf_data, + ta_conf_data_as_json +) +from spid_cie_oidc.entity.trust_chain import TrustChainBuilder +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) + +EXP = datetime_from_timestamp(exp_from_now(33)) +NOW = datetime_from_timestamp(iat_now()) + +def create_entity_config(): + JWS_RP = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt") + JWS_TA = create_jws(ta_conf_data_as_json, TA_JWK_PRIVATE, typ="entity-statement+jwt") + return [JWS_RP, JWS_TA] + + +class TrustChainTest(TestCase): + + def setUp(self): + self.tcb = TrustChainBuilder( + subject = rp_conf["sub"], + trust_anchor = ta_conf_data["sub"] + ) + + def test_get_trust_anchor_configuration(self): + self.patcher = patch( + "spid_cie_oidc.entity.trust_chain.get_entity_configurations", + return_value = create_entity_config() + ) + self.patcher.start() + TrustChainBuilder.get_trust_anchor_configuration(self.tcb) + self.patcher.stop() + + def test_get_subject_configuration_failed(self): + self.patcher = patch( + "spid_cie_oidc.entity.trust_chain.get_entity_configurations", + return_value = [] + ) + self.patcher.start() + with self.assertRaises(InvalidEntityConfiguration): + TrustChainBuilder.get_subject_configuration(self.tcb) + self.patcher.stop() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_10_schemas.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_10_schemas.py new file mode 100644 index 0000000..1123738 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/tests/test_10_schemas.py @@ -0,0 +1,21 @@ + +from django.test import TestCase +from spid_cie_oidc.entity.schemas.resolve_endpoint import ResolveRequest +from .rp_metadata_settings import rp_conf + +TA_SUB = "http://testserver.it/" + +RESOLVE_REQUEST = { + "sub": rp_conf["sub"], + "trust_anchor": TA_SUB, + "format": "json", +} + + +class SchemaTest(TestCase): + + def setUp(self) -> None: + return super().setUp() + + def test_resolve_request(self): + ResolveRequest(**RESOLVE_REQUEST) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain.py new file mode 100644 index 0000000..731cfa9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain.py @@ -0,0 +1,317 @@ +import datetime +import logging + +from collections import OrderedDict +from typing import Union, Optional + +from spid_cie_oidc.entity.policy import apply_policy + +from .exceptions import ( + InvalidEntityConfiguration, + InvalidRequiredTrustMark, + MetadataDiscoveryException, +) + +from .statements import ( + get_entity_configurations, + EntityConfiguration, +) +from .utils import datetime_from_timestamp + + +logger = logging.getLogger(__name__) + + +class TrustChainBuilder: + """ + A trust walker that fetches statements and evaluate the evaluables + + max_intermediaries means how many hops are allowed to the trust anchor + max_authority_hints means how much authority_hints to follow on each hop + + required_trust_marks means all the trsut marks needed to start a metadata discovery + at least one of the required trust marks is needed to start a metadata discovery + if this param if absent the filter won't be considered. + """ + + def __init__( + self, + subject: str, + trust_anchor: Union[str, EntityConfiguration], + httpc_params: dict = {}, + max_authority_hints: int = 10, + subject_configuration: EntityConfiguration = None, + required_trust_marks: list = [], + # TODO - prefetch cache? + # pre_fetched_entity_configurations = {}, + # pre_fetched_statements = {}, + # + **kwargs, + ) -> None: + + self.subject = subject + self.subject_configuration = subject_configuration + self.httpc_params = httpc_params + + self.trust_anchor = trust_anchor + self.trust_anchor_configuration = None + + self.required_trust_marks = required_trust_marks + self.is_valid = False + + self.tree_of_trust = OrderedDict() + self.trust_path = [] # list of valid subjects up to trust anchor + + self.max_authority_hints = max_authority_hints + # dynamically valued + self.max_path_len = 0 + self.final_metadata: dict = {} + + self.verified_trust_marks = [] + self.exp = 0 + + def apply_metadata_policy(self) -> dict: + """ + Starts the search for a trust path and, if found, applies the + metadata policies to generate the final metadata. + """ + logger.info( + f"Starting trust path search for {self.subject} to " + f"{self.trust_anchor_configuration.sub}" + ) + + start_path = [self.subject_configuration] + + valid_path = self._find_trust_path_recursive(start_path) + + if valid_path: + logger.info(f"Found a valid trust path: {[e.sub for e in valid_path]}") + self.trust_path = valid_path + + final_metadata = self.subject_configuration.payload.get("metadata", {}) + if not final_metadata: + logger.error(f"Missing metadata in subject {self.subject}") + return {} + + for i in range(len(self.trust_path) - 1, 0, -1): + superior_ec = self.trust_path[i] + subordinate_ec = self.trust_path[i - 1] + + statement = superior_ec.verified_descendant_statements.get(subordinate_ec.sub, {}) + policy = statement.get("metadata_policy", {}) + + for md_type, pol_value in policy.items(): + if final_metadata.get(md_type): + final_metadata[md_type] = apply_policy( + final_metadata[md_type], pol_value + ) + + self.final_metadata = final_metadata + self.set_exp() + return self.final_metadata + else: + logger.warning( + f"Could not find a valid trust path for {self.subject} " + f"to {self.trust_anchor_configuration.sub}" + ) + self.trust_path = [] + return {} + + def _find_trust_path_recursive(self, current_path: list) -> Optional[list]: + """ + Recursively searches for a valid trust path (Depth-First Search). + + Args: + current_path: The list of EntityConfigurations in the current path. + + Returns: + The list representing the complete path if found, otherwise None. + """ + last_entity = current_path[-1] + + # Base case (SUCCESS): we have reached the trust anchor + if last_entity.sub == self.trust_anchor_configuration.sub: + return current_path + + # Base case (FAILURE): path has exceeded the maximum allowed length + # The +1 is because max_path_len is the number of intermediaries + if len(current_path) > self.max_path_len + 1: + logger.warning( + f"Path length ({len(current_path)}) exceeds max_path_len " + f"({self.max_path_len}). Backtracking." + ) + return None + + for superior_ec in last_entity.verified_by_superiors.values(): + + # Loop prevention: do not visit an entity that is already in the path + if superior_ec.sub in [e.sub for e in current_path]: + logger.warning(f"Loop detected with entity {superior_ec.sub}. Skipping path.") + continue + + new_path = current_path + [superior_ec] + result = self._find_trust_path_recursive(new_path) + + # If the recursive call found a path, propagate the result upwards + if result: + return result + + # If the loop finishes, it means no superior led to a solution + return None + + @property + def exp_datetime(self) -> datetime.datetime: + if self.exp: # pragma: no cover + return datetime_from_timestamp(self.exp) + + def set_exp(self) -> int: + exps = [i.payload["exp"] for i in self.trust_path] + if exps: + self.exp = min(exps) + + def discovery(self) -> bool: + """ + return a chain of verified statements + from the lower up to the trust anchor + """ + logger.info(f"Starting a Walk into Metadata Discovery for {self.subject}") + self.tree_of_trust[0] = [self.subject_configuration] + + ecs_history = [] + while (len(self.tree_of_trust) - 2) < self.max_path_len: + last_path_n = list(self.tree_of_trust.keys())[-1] + last_ecs = self.tree_of_trust[last_path_n] + + sup_ecs = [] + for last_ec in last_ecs: + # Metadata discovery loop prevention + if last_ec.sub in ecs_history: + logger.warning( + f"Metadata discovery loop detection for {last_ec.sub}. " + f"Already present in {ecs_history}. " + "Discovery blocked for this path." + ) + continue + + try: + superiors = last_ec.get_superiors( + max_authority_hints=self.max_authority_hints, + superiors_hints=[self.trust_anchor_configuration], + ) + validated_by = last_ec.validate_by_superiors( + superiors_entity_configurations=superiors.values() + ) + vbv = list(validated_by.values()) + sup_ecs.extend(vbv) + ecs_history.append(last_ec) + except MetadataDiscoveryException as e: + logger.exception( + f"Metadata discovery exception for {last_ec.sub}: {e}" + ) + + if sup_ecs: + self.tree_of_trust[last_path_n + 1] = sup_ecs + else: + break + + last_path = list(self.tree_of_trust.keys())[-1] + if ( + self.tree_of_trust[0][0].is_valid + and self.tree_of_trust[last_path][0].is_valid + ): + self.is_valid = True + self.apply_metadata_policy() + + return self.is_valid + + def get_trust_anchor_configuration(self) -> None: + if isinstance(self.trust_anchor, EntityConfiguration): + self.trust_anchor_configuration = self.trust_anchor + + elif not self.trust_anchor_configuration and isinstance(self.trust_anchor, str): + logger.info(f"Starting Metadata Discovery for {self.subject}") + ta_jwt = get_entity_configurations( + self.trust_anchor, httpc_params=self.httpc_params + )[0] + self.trust_anchor_configuration = EntityConfiguration(ta_jwt) + + try: + self.trust_anchor_configuration.validate_by_itself() + except Exception as e: # pragma: no cover + _msg = ( + f"Trust Anchor Entity Configuration failed for {self.trust_anchor}. " + f"{e}" + ) + logger.error(_msg) + raise Exception(_msg) + + if self.trust_anchor_configuration.payload.get("constraints", {}).get( + "max_path_length" + ): + self.max_path_len = int( + self.trust_anchor_configuration.payload["constraints"][ + "max_path_length" + ] + ) + + def get_subject_configuration(self) -> None: + if not self.subject_configuration: + try: + jwt = get_entity_configurations( + self.subject, httpc_params=self.httpc_params + ) + self.subject_configuration = EntityConfiguration( + jwt[0], trust_anchor_entity_conf=self.trust_anchor_configuration + ) + self.subject_configuration.validate_by_itself() + except Exception as e: + _msg = f"Entity Configuration for {self.subject} failed: {e}" + logger.error(_msg) + raise InvalidEntityConfiguration(_msg) + + # Trust Mark filter + if self.required_trust_marks: + sc = self.subject_configuration + sc.filter_by_allowed_trust_marks = self.required_trust_marks + # TODO: create a proxy function that gets tm issuers ec from + # a previously populated cache + # sc.trust_mark_issuers_entity_confs = [ + # trust_mark_issuers_entity_confs + # ] + if not sc.validate_by_allowed_trust_marks(): + raise InvalidRequiredTrustMark( + "The required Trust Marks are not valid" + ) + else: + self.verified_trust_marks.extend(sc.verified_trust_marks) + + def serialize(self): + res = [] + # we have only the leaf's and TA's EC, all the intermediate EC will be dropped + ta_ec: str = "" + for stat in self.trust_path: + if not isinstance(self.trust_anchor, str): + if (self.subject == stat.sub == stat.iss): + res.append(stat.jwt) + elif (self.trust_anchor.sub == stat.sub == stat.iss): + ta_ec = stat.jwt + + if stat.verified_descendant_statements: + res.extend( + [ + i for i in stat.verified_descendant_statements_as_jwt.values() + ] + ) + if ta_ec: + res.append(ta_ec) + return res + + def start(self): + try: + self.get_trust_anchor_configuration() + self.get_subject_configuration() + self.discovery() + except Exception as e: + self.is_valid = False + logger.error(f"{e}") + raise e diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain_operations.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain_operations.py new file mode 100644 index 0000000..e292fd0 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/trust_chain_operations.py @@ -0,0 +1,203 @@ +import logging + +from django.utils import timezone +from typing import Union + +from .exceptions import InvalidTrustchain, TrustchainMissingMetadata +from .models import FetchedEntityStatement, TrustChain +from .statements import EntityConfiguration, get_entity_configurations +from .settings import HTTPC_PARAMS +from .trust_chain import TrustChainBuilder +from .utils import datetime_from_timestamp + +logger = logging.getLogger(__name__) + + +def trust_chain_builder( + subject: str, + trust_anchor: EntityConfiguration, + httpc_params: dict = HTTPC_PARAMS, + required_trust_marks: list = [] +) -> Union[TrustChainBuilder, bool]: + """ + Trust Chain builder + """ + tc = TrustChainBuilder( + subject, + trust_anchor=trust_anchor, + required_trust_marks=required_trust_marks, + httpc_params=httpc_params + ) + tc.start() + + if not tc.is_valid: + logger.error( + "The tree of trust cannot be validated for " + f"{tc.subject}: {tc.tree_of_trust}" + ) + return False + else: + return tc + + +def dumps_statements_from_trust_chain_to_db(trust_chain: TrustChainBuilder) -> list: + + entity_statements = [] + + for stat in trust_chain.trust_path: + + data = dict( + exp=datetime_from_timestamp(stat.payload["exp"]), + iat=datetime_from_timestamp(stat.payload["iat"]), + statement=stat.payload, + jwt=stat.jwt, + ) + + fes = FetchedEntityStatement.objects.filter(sub=stat.sub, iss=stat.iss) + + if fes: + fes.update(**data) + else: + fes = FetchedEntityStatement.objects.create( + sub=stat.sub, iss=stat.iss, **data + ) + + entity_statements.append(fes) + + if stat.verified_descendant_statements: + + for desc_stat_sub in stat.verified_descendant_statements: + payload = stat.verified_descendant_statements[desc_stat_sub] + jwt = stat.verified_descendant_statements_as_jwt[desc_stat_sub] + + _data = dict( + exp=datetime_from_timestamp(payload["exp"]), + iat=datetime_from_timestamp(payload["iat"]), + statement=payload, + jwt=jwt, + ) + + desc_fes = FetchedEntityStatement.objects.filter( + sub=payload["sub"], iss=payload["iss"] + ) + + if desc_fes: + desc_fes.update(**_data) + else: + desc_fes = FetchedEntityStatement.objects.create( + sub=payload["sub"], iss=payload["iss"], **_data + ) + + entity_statements.append(desc_fes) + + return entity_statements + + +def get_or_create_trust_chain( + subject: str, + trust_anchor: str, + httpc_params: dict = HTTPC_PARAMS, + required_trust_marks: list = [], + force: bool = False, +) -> Union[TrustChain, None]: + """ + returns a TrustChain model object if any available + if available it return it + if not available it create a new one + + if available and expired it return the expired one + if flag force is set to True -> renew the trust chain, update it and + return the updated one + + """ + fetched_trust_anchor = FetchedEntityStatement.objects.filter( + sub=trust_anchor, iss=trust_anchor + ) + if not fetched_trust_anchor or fetched_trust_anchor.first().is_expired or force: + + jwts = get_entity_configurations([trust_anchor], httpc_params=httpc_params) + ta_conf = EntityConfiguration(jwts[0], httpc_params=httpc_params) + + data = dict( + exp=datetime_from_timestamp(ta_conf.payload["exp"]), + iat=datetime_from_timestamp(ta_conf.payload["iat"]), + statement=ta_conf.payload, + jwt=ta_conf.jwt, + ) + + if not fetched_trust_anchor: + # trust to the anchor should be absolute trusted! + # ta_conf.validate_by_itself() + fetched_trust_anchor = FetchedEntityStatement.objects.create( + sub=ta_conf.sub, iss=ta_conf.iss, **data + ) + else: + fetched_trust_anchor.update( + exp=datetime_from_timestamp(ta_conf.payload["exp"]), + iat=datetime_from_timestamp(ta_conf.payload["iat"]), + statement=ta_conf.payload, + jwt=ta_conf.jwt, + ) + fetched_trust_anchor = fetched_trust_anchor.first() + + else: + fetched_trust_anchor = fetched_trust_anchor.first() + ta_conf = fetched_trust_anchor.get_entity_configuration_as_obj() + + tc = TrustChain.objects.filter(sub=subject, trust_anchor__sub=trust_anchor).first() + + if tc and not tc.is_active: + # if manualy disabled by staff + return None + elif force or not tc or tc.is_expired: + trust_chain = trust_chain_builder( + subject=subject, + trust_anchor=ta_conf, + required_trust_marks=required_trust_marks + ) + if not trust_chain: + raise InvalidTrustchain( + f"Trust chain for subject {subject} and " + f"trust_anchor {trust_anchor} is not found" + ) + elif not trust_chain.is_valid: + raise InvalidTrustchain( + f"Trust chain for subject {subject} and " + f"trust_anchor {trust_anchor} is not valid" + ) + elif not trust_chain.final_metadata: + raise TrustchainMissingMetadata( + f"Trust chain for subject {subject} and " + f"trust_anchor {trust_anchor} doesn't have any metadata" + ) + dumps_statements_from_trust_chain_to_db(trust_chain) + + tc = TrustChain.objects.filter( + sub=subject, trust_anchor__sub=trust_anchor + ) + data = dict( + exp=trust_chain.exp_datetime, + processing_start = timezone.localtime(), + chain=trust_chain.serialize(), + jwks = trust_chain.subject_configuration.jwks, + metadata=trust_chain.final_metadata, + parties_involved=[i.sub for i in trust_chain.trust_path], + status="valid", + trust_marks=[ + {"trust_mark_type": i.id, "trust_mark": i.jwt, "trust_mark_id": i.id} + for i in trust_chain.verified_trust_marks + ], + is_active=True, + ) + + if tc: + tc.update(**data) + tc = tc.first() + else: + tc = TrustChain.objects.create( + sub=subject, + trust_anchor=fetched_trust_anchor, + **data, + ) + + return tc diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/urls.py new file mode 100644 index 0000000..1ff1b74 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/urls.py @@ -0,0 +1,39 @@ +"""acsia_agent_proj URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/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.urls import path + +from .views import ( + entity_configuration, + historical_keys, + resolve_entity_statement +) + +_PREF = getattr(settings, "OIDC_PREFIX", "") + +urlpatterns = [ + path( + f"{_PREF}.well-known/openid-federation", + entity_configuration, + name="entity_configuration", + ), + path(f"{_PREF}resolve", resolve_entity_statement, name="oidcfed_resolve"), + path( + f"{_PREF}.well-known/openid-federation-historical-jwks", + historical_keys, + name="oidcfed_historical_keys" + ) +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/utils.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/utils.py new file mode 100644 index 0000000..adc1c0f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/utils.py @@ -0,0 +1,79 @@ +from django.utils import timezone +from django.utils.timezone import make_aware + +from secrets import token_hex +from spid_cie_oidc.entity.jwtse import unpad_jwt_head +from spid_cie_oidc.entity.settings import HTTPC_PARAMS +from spid_cie_oidc.entity.statements import get_http_url +from . import KeyUsage + +import datetime +import json +import logging + +logger = logging.getLogger(__name__) + + +def get_key(jwks, use=KeyUsage.signature): + for jwk in jwks: + if jwk['use'] == use: + return jwk + return jwks[0] + + +def iat_now() -> int: + return int(datetime.datetime.now().timestamp()) + + +def exp_from_now(minutes: int = 33) -> int: + _now = timezone.localtime() + return int((_now + datetime.timedelta(minutes=minutes)).timestamp()) + + +def datetime_from_timestamp(value) -> datetime.datetime: + return make_aware(datetime.datetime.fromtimestamp(value)) + + +def get_jwks(metadata: dict, federation_jwks: list = []) -> dict: + """ + get jwks or jwks_uri or signed_jwks_uri + """ + jwks_list = [] + if metadata.get('jwks'): + jwks_list = metadata["jwks"]["keys"] + elif metadata.get('jwks_uri'): + try: + jwks_uri = metadata["jwks_uri"] + jwks_list = get_http_url( + [jwks_uri], httpc_params=HTTPC_PARAMS + ) + jwks_list = json.loads(jwks_list[0]) + except Exception as e: + logger.error(f"Failed to download jwks from {jwks_uri}: {e}") + elif metadata.get('signed_jwks_uri'): + try: + signed_jwks_uri = metadata["signed_jwks_uri"] + jwks_list = get_http_url( + [signed_jwks_uri], httpc_params=HTTPC_PARAMS + )[0] + except Exception as e: + logger.error(f"Failed to download jwks from {signed_jwks_uri}: {e}") + return jwks_list + + +def get_jwk_from_jwt(jwt: str, provider_jwks: dict) -> dict: + """ + docs here + """ + head = unpad_jwt_head(jwt) + kid = head["kid"] + if isinstance(provider_jwks, dict) and provider_jwks.get('keys'): + provider_jwks = provider_jwks['keys'] + for jwk in provider_jwks: + if jwk["kid"] == kid: + return jwk + return {} + + +def random_token(n=254): + return token_hex(n) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/validators.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/validators.py new file mode 100644 index 0000000..9d95f2f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/validators.py @@ -0,0 +1,104 @@ +import json +from typing import Union + +from cryptojwt.jwk.jwk import key_from_jwk_dict +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ +from spid_cie_oidc.provider.settings import ( + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_PROFILES +) +from spid_cie_oidc.relying_party.settings import ( + RP_DEFAULT_PROVIDER_PROFILES, + RP_PROVIDER_PROFILES +) + +from .jwks import serialize_key +from .settings import ( + ENCRYPTION_ALG_VALUES_SUPPORTED, + ENCRYPTION_ENC_SUPPORTED, + ENTITY_TYPES, + SIGNING_ALG_VALUES_SUPPORTED +) + + +def validate_public_jwks(values: Union[dict, list]): + if isinstance(values, dict): + values = [values] + try: + for jwk_dict in values: + _k = key_from_jwk_dict(jwk_dict) + if _k.private_key(): + _pub = serialize_key(_k.public_key()) + raise ValidationError( + f"This JWK is is private {json.dumps(jwk_dict)}. " + f"It MUST be public instead, like this: {json.dumps([_pub])}." + ) + except Exception as e: + raise ValidationError(f"Not valid: {e}") + + +def validate_metadata_algs(metadata: dict): + amap = dict( + id_token_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED, + id_token_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED, + id_token_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED, + token_endpoint_auth_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED, + userinfo_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED, + userinfo_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED, + userinfo_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED, + request_object_encryption_alg_values_supported = ENCRYPTION_ALG_VALUES_SUPPORTED, + request_object_encryption_enc_values_supported = ENCRYPTION_ENC_SUPPORTED, + request_object_signing_alg_values_supported = SIGNING_ALG_VALUES_SUPPORTED, + ) + if metadata.get("openid_provider", None): + md = metadata["openid_provider"] + for k, v in amap.items(): + if k in md: + for alg in md[k]: + if alg not in v: + raise ValidationError( + f"{k} has an unsupported value {alg}. " + f"Supported algs are {v}" + ) + + +def validate_entity_metadata(value): + status = False + for i in ENTITY_TYPES: + if i in value: + status = True + if not status: + raise ValidationError( + _(f'Need to specify one of {", ".join(ENTITY_TYPES)}') + ) + if "openid_provider" in value: + schema = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + try: + schema["op_metadata"](**value["openid_provider"]) + except Exception as e: + raise ValidationError( + f"OP metadata fail {e}. " + ) + if "openid_relying_party" in value: + schema = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES] + try: + schema["rp_metadata"](**value["openid_relying_party"]) + except Exception as e: + raise ValidationError( + f"RP metadata fail {e}. " + ) + + # TODO - add wallet_provider and wallet_relying_party once standardized + + +def validate_private_jwks(values: Union[dict, list]): + if isinstance(values, dict): + values = [values] + try: + for jwk_dict in values: + _k = key_from_jwk_dict(jwk_dict) + if not _k.private_key(): + raise ValidationError(f"Can't extract a private JWK from {jwk_dict}") + except Exception as e: + raise ValidationError(f"Not valid: {e}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/views.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/views.py new file mode 100644 index 0000000..20cf3a8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/views.py @@ -0,0 +1,257 @@ +import logging +from urllib.parse import urlsplit, urlunsplit + +from django.conf import settings +from djagger.decorators import schema +from django.http import Http404 +from django.http import HttpResponse +from django.http import JsonResponse + +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.schemas.resolve_endpoint import ( + ResolveRequest, ResolveResponse, ResolveErrorResponse +) +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain + +from .models import ( + FederationEntityConfiguration, + FederationHistoricalKey, + TrustChain, + StaffToken +) +from .settings import HTTPC_PARAMS +from .statements import OIDCFED_FEDERATION_WELLKNOWN_URL +from .utils import iat_now + +logger = logging.getLogger(__name__) + + +def _swap_scheme(url: str) -> str: + parts = urlsplit(url) + if parts.scheme == "http": + return urlunsplit(("https", parts.netloc, parts.path, parts.query, parts.fragment)) + if parts.scheme == "https": + return urlunsplit(("http", parts.netloc, parts.path, parts.query, parts.fragment)) + return url + + +def get_subs_from_wellknown(request, wkuri :str): + sub = request.build_absolute_uri().split(wkuri)[0].rstrip("/") + sub_values = [] + for candidate in (sub, _swap_scheme(sub)): + for value in (candidate, f"{candidate}/"): + if value not in sub_values: + sub_values.append(value) + return sub_values + + +def entity_configuration(request): + """ + OIDC Federation Entity Configuration at + .well-known/openid-federation + """ + + _sub_values = get_subs_from_wellknown(request, OIDCFED_FEDERATION_WELLKNOWN_URL) + + conf = FederationEntityConfiguration.objects.filter( + # TODO: check for reverse proxy and forwarders ... + sub__in=_sub_values, + is_active=True, + ).first() + if not conf: # pragma: no cover + raise Http404() + + if request.GET.get("format") == "json": # pragma: no cover + return JsonResponse(conf.entity_configuration_as_dict, safe=False) + else: + return HttpResponse( + conf.entity_configuration_as_jws, content_type="application/entity-statement+jwt" + ) + + +@schema( + methods=['GET'], + get_request_schema = { + "application/x-www-form-urlencoded": ResolveRequest + }, + get_response_schema = { + "400": ResolveErrorResponse, + "404": ResolveErrorResponse, + "200": ResolveResponse + }, + tags = ['Federation API'] +) +def resolve_entity_statement(request, format: str = "jose"): + """ + resolves the final metadata of its descendants + + In this implementation we only returns a preexisting + Metadata if it's valid + we avoid any possibility to trigger a new Metadata discovery if + """ + if not all((request.GET.get("sub"), request.GET.get("trust_anchor"))): + return JsonResponse( + { + "error": "invalid_request", + "error_description": "sub and trust_anchor parameters are REQUIRED." + }, + status=400 + ) + + iss = FederationEntityConfiguration.objects.filter(is_active=True).first() + + _q = dict( + sub=request.GET["sub"], + trust_anchor__sub=request.GET["trust_anchor"], + is_active=True + ) + + # gets the cached one + entity = TrustChain.objects.filter(**_q).first() + + # only with privileged actors with staff token can triggers a new trust chain + staff_token_head = request.headers.get("Authorization", None) + if staff_token_head: + staff_token = StaffToken.objects.filter( + token = staff_token_head + ).first() + if staff_token.is_valid: + try: + # a staff token get a fresh trust chain on each call + entity = get_or_create_trust_chain( + httpc_params = HTTPC_PARAMS, + required_trust_marks = getattr( + settings, "OIDCFED_REQUIRED_TRUST_MARKS", [] + ), + subject=_q["sub"], + trust_anchor=_q["trust_anchor__sub"], + force = True + ) + except Exception as e: + logger.error( + f"Failed privileged Trust Chain creation for {_q['sub']}: {e}" + ) + + if not entity: + return JsonResponse( + { + "error": "not_found", + "error_description": "entity not found" + }, + status=404 + ) + + metadata = entity.metadata + # Filter by entity_type if requested (OpenID Federation 8.3.1) + entity_types = request.GET.getlist("entity_type") + if entity_types: + metadata = { + k: v for k, v in (metadata or {}).items() + if k in entity_types + } + + res = { + "iss": iss.sub, + "sub": request.GET["sub"], + "iat": entity.iat_as_timestamp, + "exp": entity.exp_as_timestamp, + "trust_marks": entity.trust_marks, + "metadata": metadata, + "trust_chain": entity.chain + } + + if request.GET.get("format") == "json" or format == "json": + return JsonResponse(res, safe=False) + + jwk = iss.jwks_fed[0] + signed = create_jws( + res, + jwk, + alg=iss.default_signature_alg, + protected={"typ": "resolve-response+jwt", "kid": jwk.get("kid")}, + ) + return HttpResponse( + signed, + content_type="application/resolve-response+jwt", + ) + + +def openid_jwks(request, metadata_type:str, resource_type:str): + """ + resource_tytpe = set(jwks_uri, jwks.jose) + """ + _sub = request.build_absolute_uri().rsplit(resource_type)[0] + _lookup = _sub.replace(f"/{metadata_type}/", "").rstrip("/") + _lookup_values = [_lookup, _swap_scheme(_lookup)] + conf = FederationEntityConfiguration.objects.filter( + # TODO: check for reverse proxy and forwarders ... + sub__in=_lookup_values, + is_active=True, + ).first() + if not conf: # pragma: no cover + raise Http404() + content = conf.entity_configuration_as_dict['metadata'].get( + metadata_type, {} + ).get("jwks", {}) + + if not content: + raise Http404() + + if resource_type == 'jwks.json': + return JsonResponse(content, safe=False) + elif resource_type == 'jwks.jose': + return HttpResponse( + create_jws(content, conf.jwks_fed[0]), + content_type="application/jose" + ) + else: + raise Http404() + + +def openid_connect_jwks_uri(request, metadata_type, resource_type="jwks.json"): + """ + OpenID Connect default jwks_uri + """ + return openid_jwks(request, metadata_type, resource_type) + + +def openid_connect_signed_jwks_uri(request, metadata_type, resource_type="jwks.jose"): + """ + OpenID Federation signed_jwks_uri + """ + return openid_jwks(request, metadata_type, resource_type) + + +def historical_keys(request): + """ + OIDC Federation Entity Configuration at + .well-known/openid-federation + """ + + sub_values = get_subs_from_wellknown(request, '.well-known/openid-federation-historical-jwks') + keys = FederationHistoricalKey.objects.filter(entity__sub__in=sub_values) + + entity = keys.first().entity if keys.first() else None + + reg = { + "iss": entity.sub if entity else sub_values[0], + "iat": iat_now(), + "keys": [] + } + + for i in keys: + reg["keys"].append(i.as_dict) + + if request.GET.get("format") == "json": # pragma: no cover + return JsonResponse(reg, safe=False) + else: + res = create_jws( + reg, + entity.jwks_fed[0], + typ="jwk-set+jwt", + # **kwargs, + ) + + return HttpResponse( + res, content_type="application/jwk-set+jwt" + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/x509.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/x509.py new file mode 100644 index 0000000..8539a82 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/entity/x509.py @@ -0,0 +1,101 @@ +import base64 + +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes, serialization +from django.utils import timezone + +from typing import Union + +from django.conf import settings + + +class X509Issuer: + + def __init__( + self, + private_key: bytes, + public_key: bytes, + subject_data: Union[bytes, None] = None, + is_ca_or_int: bool = False, + path_length: Union[int, None] = None + ) -> x509.CertificateBuilder: + """ + returns an X.509 certificate + """ + # private_key = COSEKey.from_bytes(self.private_key.encode()) + # TODO: add CDP using CRL + + if subject_data.get("X509_COMMON_NAME"): + cn = subject_data["X509_COMMON_NAME"] + else: + cn = getattr(settings, "X509_COMMON_NAME") + + subject_name = [ + x509.NameAttribute(NameOID.COMMON_NAME, cn) + ] + [ + x509.NameAttribute(NameOID.getattr(i), subject_data[i]) + for i in ( + 'COUNTRY_NAME', + 'X509_STATE_OR_PROVINCE_NAME', + 'X509_LOCALITY_NAME', + 'X509_ORGANIZATION_NAME', + ) if subject_data.get(i) + ] + + _basic_constraints = dict(ca=is_ca_or_int) + _basic_constraints["path_length"] = path_length + + self.cert = ( + x509.CertificateBuilder() + .issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, settings.X509_COUNTRY_NAME), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, settings.X509_STATE_OR_PROVINCE_NAME), + x509.NameAttribute(NameOID.LOCALITY_NAME, settings.X509_LOCALITY_NAME), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, settings.X509_ORGANIZATION_NAME), + x509.NameAttribute(NameOID.COMMON_NAME, settings.X509_COMMON_NAME), + ]) + ) + .subject_name(x509.Name(subject_name)) + .public_key(public_key) + .serial_number(x509.random_serial_number()) + .not_valid_before( + getattr(settings, "X509_NOT_VALID_BEFORE", None) or + subject_data.get('X509_NOT_VALID_BEFORE') or + (timezone.localtime() - timezone.timedelta(minutes=12)) + ) + .not_valid_after( + getattr(settings, "X509_NOT_VALID_AFTER", None) or + subject_data.get('X509_NOT_VALID_AFTER') or + (timezone.localtime() + timezone.timedelta(days=365)) + ) + .add_extension( + x509.BasicConstraints(**_basic_constraints), + critical=True, + ) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName(cn), + x509.UniformResourceIdentifier(subject_data.get('entity_id')), + ]), + critical=False + ) + .sign(private_key, hashes.SHA256()) + ) + + @property + def der(self): + return self.cert.public_bytes( + getattr(serialization.Encoding, 'DER') + ) + + @property + def pem(self): + return self.cert.public_bytes( + getattr(serialization.Encoding, 'PEM') + ) + + @property + def x5c(self): + return [base64.b64encode(self.der).decode('utf-8')] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/admin.py new file mode 100644 index 0000000..4132e54 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin +from django.contrib import messages +from django.db import IntegrityError + +from .models import OnBoardingRegistration +from spid_cie_oidc.authority.models import FederationDescendant, FederationDescendantContact + + +@admin.register(OnBoardingRegistration) +class OnBoardingRegistrationAdmin(admin.ModelAdmin): + + @admin.action(description='enable descendant') + def enable_as_descendant(modeladmin, request, queryset): + for entity_onboarded in queryset: + name = entity_onboarded.organization_name + sub = entity_onboarded.url_entity + jwks = entity_onboarded.public_jwks + _type = entity_onboarded.type + contact = entity_onboarded.contact + + try: + entity = FederationDescendant.objects.create( + name = name, + sub = sub, + type = _type, + jwks = jwks, + is_active = True, + status = "valid" + ) + FederationDescendantContact.objects.create( + entity = entity, + contact = contact, + type = "email" + ) + entity_onboarded.status = "onboarded" + entity_onboarded.save() + except IntegrityError: # pragma: no cover + messages.error(request, f"Already exists a descendant with subject: {sub}") + + list_display = ( + "organization_name", + "url_entity", + "type", + "authn_buttons_page_url", + "public_jwks", + ) + actions = [enable_as_descendant] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/apps.py new file mode 100644 index 0000000..4927734 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class SpidCieOidcOnboardingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.onboarding" + verbose_name = "SPID/CIE OIDC Federation OnBoarding" + label = "spid_cie_oidc_onboarding" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/forms.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/forms.py new file mode 100644 index 0000000..7a36f99 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/forms.py @@ -0,0 +1,134 @@ +import logging +from django import forms +from django.utils.translation import gettext_lazy as _ +from spid_cie_oidc.entity.validators import validate_public_jwks +from spid_cie_oidc.authority.validators import validate_entity_configuration +from .validators import unique_entity_url +from spid_cie_oidc.entity.models import ENTITY_TYPES +from django.conf import settings + +logger = logging.getLogger(__name__) + +try: + OIDCFED_TRUST_ANCHORS = getattr( + settings, "OIDCFED_TRUST_ANCHORS" + ) +except AttributeError: # pragma: no cover + OIDCFED_TRUST_ANCHORS = [] + logger.warning( + "OIDCFED_TRUST_ANCHORS not configured in your settings file." + ) + + +class OnboardingRegistrationForm(forms.Form): + + organization_name = forms.CharField( + initial="", + label=_("Organization Name"), + error_messages={"required": _("Enter your organization name")}, + ) + + url_entity = forms.URLField( + initial="", + label=_("Entity URL"), + error_messages={"required": _("Enter the entity unique identifier (HTTP URL)")}, + validators=[validate_entity_configuration, unique_entity_url], + ) + + authn_buttons_page_url = forms.URLField( + initial="", + label=_("URL of the login page, if available"), + error_messages={ + "required": _( + "Enter the url of the page where the SPID/CIE button is available" + ) + }, + required=False, + ) + + auth_request_url = forms.CharField( + initial="", + label=_("Authentication request URL, if available"), + required = False + ) + + contact = forms.EmailField( + initial="", + label=_("Contact email"), + error_messages={"required": _("Enter your contact email")}, + ) + + type = forms.ChoiceField( + choices=[(i, i) for i in ENTITY_TYPES], + label="", + error_messages={"required": _("Select a entity type")}, + ) + + public_jwks = forms.JSONField( + initial=list, + label=_("Public Federation JWKs"), + error_messages={ + "required": _( + "Enter the public JWKs published in your Entity Configuration, " + "as double check." + ) + }, + validators=[validate_public_jwks], + ) + + +class OnboardingCreateTrustChain(forms.Form): + + sub = forms.URLField( + initial="", + label=_("url of subject"), + error_messages={"required": _("Enter the subject")}, + ) + + type = forms.ChoiceField( + choices=[(i, i) for i in ENTITY_TYPES], + label="OpenID Connect Federation entity type", + error_messages={"required": _("Select a entity type")}, + ) + + trust_anchor = forms.ChoiceField( + choices=[(i, i) for i in OIDCFED_TRUST_ANCHORS], + label=_("url of the trust anchor"), + error_messages={"required": _("Enter the url of trust anchor")}, + ) + + +class OnboardingValidatingTrustMarkForm(forms.Form): + + sub = forms.URLField( + initial="", + label=_("Url of subject"), + required=False + ) + + id = forms.URLField( + initial="", + label=_("Id"), + required=False + ) + + trust_mark = forms.CharField( + initial = "", + label=_("Enter trust mark"), + required=False + ) + + +class OnboardingDecodeForm(forms.Form): + + jwt = forms.CharField( + initial="", + label=_("jwt"), + error_messages={"required": _("Enter a jwt")}, + ) + + jwk = forms.JSONField( + initial=dict, + label=_("jwk"), + required=False, + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0001_initial.py new file mode 100644 index 0000000..739fe7f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 4.0.2 on 2022-02-22 08:39 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import spid_cie_oidc.entity.validators + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="OnBoardingRegistration", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "organization_name", + models.CharField(help_text="Organization Name. ", max_length=254), + ), + ( + "url_entity", + models.URLField( + help_text="URL of the Entity.", max_length=254, unique=True + ), + ), + ( + "authn_buttons_page_url", + models.URLField( + help_text="URL of the page where the SPID/CIE button is available.", + max_length=254, + ), + ), + ( + "public_jwks", + models.JSONField( + default=dict, + help_text="Public jwks of the Entities", + validators=[ + spid_cie_oidc.entity.validators.validate_public_jwks + ], + ), + ), + ( + "status", + models.CharField( + choices=[ + ("onboarded", "onboarded"), + ("failed", "failed"), + ("processing", "processing"), + ("caquired", "acquired"), + ], + default="acquired", + max_length=33, + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "OnBoarding Registration", + "verbose_name_plural": "OnBoarding Registrations", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0002_alter_onboardingregistration_options_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0002_alter_onboardingregistration_options_and_more.py new file mode 100644 index 0000000..fe3e72d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0002_alter_onboardingregistration_options_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.0.2 on 2022-02-23 12:23 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_onboarding", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="onboardingregistration", + options={ + "ordering": ["created"], + "verbose_name": "OnBoarding Registration", + "verbose_name_plural": "OnBoarding Registrations", + }, + ), + migrations.AddField( + model_name="onboardingregistration", + name="created", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="onboardingregistration", + name="modified", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0003_alter_onboardingregistration_options.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0003_alter_onboardingregistration_options.py new file mode 100644 index 0000000..8123c6e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0003_alter_onboardingregistration_options.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.2 on 2022-02-25 16:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "spid_cie_oidc_onboarding", + "0002_alter_onboardingregistration_options_and_more", + ), + ] + + operations = [ + migrations.AlterModelOptions( + name="onboardingregistration", + options={ + "ordering": ["-created"], + "verbose_name": "OnBoarding Registration", + "verbose_name_plural": "OnBoarding Registrations", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0004_alter_onboardingregistration_public_jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0004_alter_onboardingregistration_public_jwks.py new file mode 100644 index 0000000..1d6a88b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0004_alter_onboardingregistration_public_jwks.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-11 13:16 + +from django.db import migrations, models +import spid_cie_oidc.entity.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0003_alter_onboardingregistration_options'), + ] + + operations = [ + migrations.AlterField( + model_name='onboardingregistration', + name='public_jwks', + field=models.JSONField(default=list, help_text='Public jwks of the Entities', validators=[spid_cie_oidc.entity.validators.validate_public_jwks]), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0005_alter_onboardingregistration_status.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0005_alter_onboardingregistration_status.py new file mode 100644 index 0000000..95a3383 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0005_alter_onboardingregistration_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-03-16 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0004_alter_onboardingregistration_public_jwks'), + ] + + operations = [ + migrations.AlterField( + model_name='onboardingregistration', + name='status', + field=models.CharField(choices=[('onboarded', 'onboarded'), ('failed', 'failed'), ('processing', 'processing'), ('acquired', 'acquired')], default='acquired', max_length=33), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0006_onboardingregistration_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0006_onboardingregistration_type.py new file mode 100644 index 0000000..f03c249 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0006_onboardingregistration_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-03-18 17:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0005_alter_onboardingregistration_status'), + ] + + operations = [ + migrations.AddField( + model_name='onboardingregistration', + name='type', + field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('oauth_resource', 'oauth_resource')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0007_onboardingregistration_contact.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0007_onboardingregistration_contact.py new file mode 100644 index 0000000..d2637e6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0007_onboardingregistration_contact.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.2 on 2022-03-22 16:50 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0006_onboardingregistration_type'), + ] + + operations = [ + migrations.AddField( + model_name='onboardingregistration', + name='contact', + field=models.CharField(default=datetime.date(2022, 3, 22), help_text='any kind of contact type, usually an email.', max_length=255), + preserve_default=False, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0008_onboardingregistration_auth_request_url.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0008_onboardingregistration_auth_request_url.py new file mode 100644 index 0000000..887189e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0008_onboardingregistration_auth_request_url.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-03-28 13:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0007_onboardingregistration_contact'), + ] + + operations = [ + migrations.AddField( + model_name='onboardingregistration', + name='auth_request_url', + field=models.CharField(blank=True, help_text='SPID/CIE authentication request trigger url.', max_length=254, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0009_alter_onboardingregistration_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0009_alter_onboardingregistration_type.py new file mode 100644 index 0000000..affb367 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0009_alter_onboardingregistration_type.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.1 on 2023-07-09 15:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_onboarding", "0008_onboardingregistration_auth_request_url"), + ] + + operations = [ + migrations.AlterField( + model_name="onboardingregistration", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0010_alter_onboardingregistration_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0010_alter_onboardingregistration_type.py new file mode 100644 index 0000000..182e282 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0010_alter_onboardingregistration_type.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.7 on 2023-11-14 14:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("spid_cie_oidc_onboarding", "0009_alter_onboardingregistration_type"), + ] + + operations = [ + migrations.AlterField( + model_name="onboardingregistration", + name="type", + field=models.CharField( + blank=True, + choices=[ + ("federation_entity", "federation_entity"), + ("openid_relying_party", "openid_relying_party"), + ("openid_provider", "openid_provider"), + ("openid_credential_issuer", "openid_credential_issuer"), + ("oauth_resource", "oauth_resource"), + ("wallet_provider", "wallet_provider"), + ("wallet_relying_party", "wallet_relying_party"), + ], + default="openid_relying_party", + help_text="OpenID Connect Federation entity type", + max_length=33, + ), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0011_alter_onboardingregistration_type.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0011_alter_onboardingregistration_type.py new file mode 100644 index 0000000..1878871 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/0011_alter_onboardingregistration_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2026-03-23 15:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_onboarding', '0010_alter_onboardingregistration_type'), + ] + + operations = [ + migrations.AlterField( + model_name='onboardingregistration', + name='type', + field=models.CharField(blank=True, choices=[('federation_entity', 'federation_entity'), ('openid_relying_party', 'openid_relying_party'), ('openid_provider', 'openid_provider'), ('openid_credential_issuer', 'openid_credential_issuer'), ('oauth_resource', 'oauth_resource'), ('wallet_provider', 'wallet_provider'), ('openid_credential_verifier', 'openid_credential_verifier')], default='openid_relying_party', help_text='OpenID Connect Federation entity type', max_length=33), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/models.py new file mode 100644 index 0000000..e301830 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/models.py @@ -0,0 +1,81 @@ +from django.db import models +from django.contrib.auth import get_user_model +from django.utils.translation import gettext as _ + +from spid_cie_oidc.entity.abstract_models import TimeStampedModel +from spid_cie_oidc.entity.validators import validate_public_jwks +from spid_cie_oidc.entity.models import ENTITY_TYPES + + +class OnBoardingRegistration(TimeStampedModel): + + organization_name = models.CharField( + max_length=254, help_text=_("Organization Name. ") + ) + + url_entity = models.URLField( + max_length=254, + blank=False, + null=False, + unique=True, + help_text=_("URL of the Entity."), + ) + + authn_buttons_page_url = models.URLField( + max_length=254, + blank=False, + null=False, + help_text=_("URL of the page where the SPID/CIE button is available."), + ) + + auth_request_url = models.CharField( + max_length=254, + blank=True, + null= True, + help_text=_("SPID/CIE authentication request trigger url."), + ) + + public_jwks = models.JSONField( + blank=False, + help_text=_("Public jwks of the Entities"), + default=list, + validators=[validate_public_jwks], + ) + + type = models.CharField( + max_length=33, + blank=True, + default="openid_relying_party", + choices=[(i, i) for i in ENTITY_TYPES], + help_text=_("OpenID Connect Federation entity type"), + ) + + contact = models.CharField( + max_length=255, + blank=False, + null=False, + help_text=_("any kind of contact type, usually an email."), + ) + + status = models.CharField( + max_length=33, + choices=( + ("onboarded", "onboarded"), + ("failed", "failed"), + ("processing", "processing"), + ("acquired", "acquired"), + ), + default="acquired", + ) + + created_by = models.ForeignKey( + get_user_model(), blank=True, null=True, on_delete=models.PROTECT + ) + + class Meta: + verbose_name = "OnBoarding Registration" + verbose_name_plural = "OnBoarding Registrations" + ordering = ["-created"] + + def __str__(self): + return f"{self.organization_name} {self.url_entity}" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/SPID-e-CIE-696x383.jpg b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/SPID-e-CIE-696x383.jpg new file mode 100644 index 0000000000000000000000000000000000000000..394102f9cf4767958bc1ede0a51fb3e928588432 GIT binary patch literal 12269 zcmeHtc{tQ-`}oIF_9n~N8AXwONhC}vYe%w|Z4QzxA=#O!h{%#X6tZNC$u7HW5!p=$ zjeTcq4QBa{_Z;Wj>3YxdE`R)f*YzFOb3JB0&*!=K`?>G?xrv{NUxDLVYMN>Q85tSy zGwBZ?5`YVr^;A_1bo3PXY;TDdAdqhLa%Er#Y zdFr&F&>7*gvghRF&nsNMqN=8@p{b>BU}$7~Z#_uV}_y}W$_0v`tj zKM4tqj){$nPk5e~l%A29^*TEzH?O#)w5+_M@?F*YrskH`wh!$cef6zKN`GrNy#^%=c&NuAt-T_@?0L3q4{YKdjbTN|XBB!LJprksWi;UcpG$tz4t zkW3F10!ZVyt||-gCxiZ1VSX>r2Y)8i?-4ry@>c-<4EX==&kmS(Xa&D9?SMh#e=rUt zdMKF#>HL|54mSr#X7*R(cV>8iz>li)BhW+O|H^zI=|gonp!xu*1Hiwd{E))`R4opv zKLl_9jH!_vq*rnOn%WA z5#J^PB}Oom15sT1G;s>L@wxCrFpl9Z)MSeY93ymXDi8aqy_LBA@jgzU2a9^(VqQCpKObjeRVI}&Wg7f?lYaX%N3L$^m!qjsVHpddeWX~ zHG7el*WeOrU>)-rmTz>w>i!lZ~T-|-GVO8HRQ zKhD2M-(fxlj8fDTiqz~|E&SwilX)6qqzGyrN)7bWNyEZbV|3RG%8`8MH&4F|f_@bM z<63-S!O9n~3M>5ONoPv$&>!(?41qsZfM2S40heP|g<7kA>?V+-Hk1@unL&nxojeQJ zx4Bgkaibl-T&aIg$r$J)j zfU$n|qK8RA@t4o$jqVipquJ=QF<^%}LAGS8r3%jI;^k68o$Ziiai=WH3x0%L&YuMM2o5xZLzNg6hK(P}L> zb*zq2BP+SG{9LC>uVBjSioOaB+?Awq9+&m^q%(?J)LY0IRm=G1V@DVg4f4EfDf;7@ZxPPv>4)Y_Cau1^ISp>xB{P}Mw{P>8ZJ@r6jsq(} z6pRY+A*pY^DfX%?#;&cu;_!fW6%O$`Jy_%MdRe1f!J)k0?c}PsZ=eNs`vbVSk-&=dE z+rPKT*22rE%!+ZdV>YLI7Ri|yy@jOR`J1CUDDsW&x_`8X9O$XjKnhBwx*ri>-T`s_>RrS8EK-E_qP4d`%Jv$tA zjCwR)_`+1ZnX$W%e1b5vR%?Ronp3&_{9segAb!*n>sKM)Ayg2~)i{0vL+hPn5$Wp* z(UIc60A0&y@rlW07q_pLkm(LQC1;mkEJoEl(iSA%G9HDH&3ySD5{FOJq4am0sN?Z- zF2abkymoZt3CjFz@QnNN`7Mt9?9T-M{HAS4aEfT(&-QC)>9CW!@va;{XH496HT%Yg zN`Bg4=|(N*4xe9ZvX){$V`}PQ`{luGC`KXx!fLUCULUGg-hDD`?|xzi&u_gn@4JO` zqLN)_|HAXrDPyf{-1(i#pUkAr4r(BpNa3tmutt~*jVwm3%om^aW@5q?YF`ZsY}M@hYS$L*(-eGN9lxwy4M?bjz8_k9h!Roo2dZG?MkQMn8f4JoqWY8{>+;0*CsN zxsW#&!(!rM5+k<4Ez(bqw|Yt`)x8*!?s^-QoGl!3)}Fh9S?Em81(}cYNSC+xq>78L zi9qv()BJIdCWN6(D$uxFUNJjfL!}=*EBho~4ZII~uH~%<$rgZg8eQ*})JqV~#a{5A zE$)zVjg$$V_LgPYeHA@!9Ar0so-wpozEMx%(V2jxZRdearye2XmG!lTZt&7j(O5_M z-W_S6L;QI#Kgy0WvJDIgAy8wpn>?Z|@aEu|F!-8OTrJR>6+11+abt|`W2F06 zQ1jeK)lKN_r%tKW7=ckDFjB^TAdvRll%I1y#&xf1r^G2{u*q) zOa#)(JfUnvU^;mTvE72$lFdt{Q|1}-Q+lfR$amhEb@9smTbDAAP~HO#72se*AlkVQ zfdZ3u9;&;{XaX*g)l06r7dh{*M5~vTP^o=0lL+WfcL=4|t17b`dh0LiVpU_Bzn;deylBO^RdkD^=C@?365lh6c7GUM{+Qpv8BkDu$wx=TKBIdy;8xCg;|LdH+N z*0MY9tT;Tmfl#(FT#pNr{5G<*sr*!w2)H7hmWcrW%p&Pv$=vN5N+1F!F6itnr|um` z=D^P(+NFl-o+0M3OEY7Vw)&-ztzqca379U9WQEbz1^#?QpcjMcoP(`BhhgAF$^mDH zfP1qKJZwER5KjNJ6)vpd%u*Jf?!Afp*6PBBfu_l>q#lP0E)W3*CX{wr|IqBL#d zlHn&wmfoNo3>+KmAatE;-%;|IgPf0M-(04EESd5uVZ~gh5R6@Kc-)-^d-*lO>sB7B zDXj8rAUG#GMSAj|S6anbjY)VEO4pu6H;{)*afG_F$Xco_5LG) zD2^MqG(9uU$&^mhGmpHJh$K*#_KA!6UCwTJd+JM&NUX}^O{r%MKF^N9Q?|>};obUn zdtDLfidWc2%+%!#%Nw2E9FwUYDTmsHA>LCR^}*MPA|^n(YK5HfmfBx!RzIqh;kEL0 z81;`p3njcAQc_=SI$Tg&oNqC|X83R#VzQTt21P>AXV>RVv#Iopn~f=|BhiR9?)?Cm z2+DlDq{Rs>!9F~unCv9ZZu(`jwN*){=PLocHYa~1w5eLcM*+F9n<8!)=H;-sHs%A} z+=ZCxYAEQTX6MGAcq3y8Vpw4WH8yWnHH-+@!Zi9vHK-^AMN_Zq%!KZh7x|f@O+oB- zB?~@}Yy>zWE-KrlmVjLMwvhT~IxT1`WUv;IID-1i#IHue3BuU;w$@Ukicmi(1e(Xe zUqn~C_{zOP<>ip!gwI3T@F<#ASN*3|wvE@0F}X&Ukg^WLXwo_UW0?0Je+O@|Eia)* zz3z+DdiTh7HMdgUJAVVkL9wUp^dM8Ti@)^1V#m|0i`cztF^)bbxk`mGh>8KIs zkBw*8_-0HB=7Wotr?n(l-c?`VeK-kYUPxi;hv*O5Za57-x^>2?(+8l^M{N}H!Fy5| z#7e_8Kx?h$#%CE5;L_lCvn-rEf<2eWWy^>pepn-4VH*eIK+Py5# z?zmBHEf3Hq&BBiof%bF#h>2t(pjPIFC{4vZ9Z|=_

WPXLxz#!#WIXN@DOj8HO{L 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 Kb+Dd{IQ%~)UL8LG literal 0 HcmV?d00001 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/logo-it.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/logo-it.svg new file mode 100644 index 0000000..48aa61c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/logo-it.svg @@ -0,0 +1,10 @@ + + + logo-it-2021 + + + + + + + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/base.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/base.html new file mode 100644 index 0000000..d7d1ff1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/base.html @@ -0,0 +1,202 @@ + +{% extends 'bootstrap-italia-base.html' %} + +{% load static %} +{% load i18n %} + +{% block extra_head %} + {{ block.super }} +{% endblock extra_head %} + +{% block page_title %} +{% trans "OIDC Federation onboarding system" %} +{% endblock page_title %} + + +{% block header_slim_org_url %} +https://www.spid.gov.it/ +{% endblock header_slim_org_url %} + + +{% block header_slim_org_name %} + {% trans "OIDC Federation onboarding system" %} +{% endblock header_slim_org_name %} + + +{% block header_slim_mobile_org_name %} + {% trans "OIDC Federation onboarding system" %} +{% endblock header_slim_mobile_org_name %} + + +{% block header_mobile_arrow %}{% endblock header_mobile_arrow %} +{% block header_mobile_slim_menu %}{% endblock header_mobile_slim_menu %} + + +{% block header_center_logo %} +{% endblock header_center_logo %} + + +{% block header_center_org_name %} + {% trans "OIDC Federation onboarding system" %} +{% endblock header_center_org_name %} + + +{% block header_center_org_subname %} + {% trans "OIDC Federation onboarding system" %} +{% endblock header_center_org_subname %} + + +{% block menu_links %} + +

+ + + + + +{% endblock menu_links %} + +{% block container %} +
+
+
+
+
+
+
+
+ {% block centered_container %}{% endblock %} +
+
+
+
+
+
+
+
+{% endblock container %} + + +{% block footer_logo %} +{% endblock footer_logo %} + + +{% block footer_org_name %} + {% trans "OIDC Federation onboarding system" %} +{% endblock footer_org_name %} + + +{% block footer_org_subname %} + {% trans "OIDC Federation onboarding system" %} +{% endblock footer_org_subname %} + +{% block extra_scripts %} +{% block bootstrap_italia_validation %} + +{% endblock bootstrap_italia_validation %} +{% endblock extra_scripts %} + +{% block footer_menu_section %} +{% endblock footer_menu_section %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_apply_policy.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_apply_policy.html new file mode 100644 index 0000000..18c73cb --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_apply_policy.html @@ -0,0 +1,65 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+ +

+ {% trans "Apply policy" %} +

+ +

+ {% trans "Adheres a metadata policy to a metadata statement" %} +

+
+ {% csrf_token %} +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+
+
+
+ +
+ +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_jwk.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_jwk.html new file mode 100644 index 0000000..a91e7f1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_jwk.html @@ -0,0 +1,47 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+ +

+ {% trans "Convert jwk to pem" %} +

+ +

+ {% trans "Enter a jwk in the field below to encode it in PEM format." %} +

+
+ {% csrf_token %} +
+
+ + +
+
+ {% if pem %} +

+ {% if jwk_type == 'private' %} + {% trans "Private key" %} + {% else %} + {% trans "Public key" %} + {% endif %} +

+
+
+ +
+
+ {% endif %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_pem.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_pem.html new file mode 100644 index 0000000..6ce4a6e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_convert_pem.html @@ -0,0 +1,51 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+ +

+ {% if pem_type == 'private' %} + {% trans "Convert private pem to jwk" %} + {% else %} + {% trans "Convert Public pem to jwk" %} + {% endif %} +

+ +

+ {% trans "Enter a pem in the field below to decode it in jwk format." %} +

+
+ {% csrf_token %} +
+
+ + +
+
+ {% if jwk %} +

+ {% if pem_type == 'private' %} + {% trans "Private pem" %} + {% else %} + {% trans "Public pem" %} + {% endif %} +

+
+
+ +
+
+ {% endif %} + +
+ +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_decode_jwt.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_decode_jwt.html new file mode 100644 index 0000000..34a3748 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_decode_jwt.html @@ -0,0 +1,75 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+ +

+ {% trans "JWT decode and verification" %} +

+ +

+ {% trans "Submit a jwt to decode it, if you submit also a jwk you'll decrypt or verify it." %} +

+
+ {% csrf_token %} +
+
+ {% csrf_token %} + {% for form_field in form %} +
+ {% if form_field.name == 'jwt' %} +
+ + +
+ {% else %} +
+ + +
+ + {% endif %} + {{ form_field.errors }} +
+ {% endfor %} +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_entities.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_entities.html new file mode 100644 index 0000000..1b519d7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_entities.html @@ -0,0 +1,80 @@ + +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} +
+

+ {% trans "Entities onboarded"%} +

+ + + + + + + + + + + {% for entity in entities %} + + + + + {% if entity.status == 'processing' %} + + {% elif entity.status == 'failed' %} + + {% elif entity.status == 'onboarded' %} + + {% else %} + + {% endif %} + + {% endfor %} + +
#{% trans "organization name" %}{% trans "url entity" %}{% trans "status" %}
{{ forloop.counter }}{{ entity.organization_name }}{{ entity.url_entity }}{{ entity.status }}{{ entity.status }}{{ entity.status }}{{ entity.status }}
+ + + + +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_jwk.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_jwk.html new file mode 100644 index 0000000..fced077 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_jwk.html @@ -0,0 +1,31 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} +
+

+ {% trans "Create a JWK" %} +

+ +

+ {% trans "In this page you have a brand new JWK. Refresh the page to gent another one." %} +

+ +
+
+ +
+
+ +

+ {% trans "Here the public version of the previous private JWK." %} +

+
+
+ +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_landing.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_landing.html new file mode 100644 index 0000000..31abedc --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_landing.html @@ -0,0 +1,82 @@ + +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + + +{% block container %} +
+
+
+
+
+
+
+
+
+
+
+
+
+ superman +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+

+ {% trans "Welcome" %} +

+ + +

+ {% blocktrans %} + The SPID/CIE onboarding demostration system wants to exemplify what + information is required to + register and test a leaf entity within the OIDC Federation. + {% endblocktrans %} +

+ +

+ {% blocktrans %} + In this demonstration consider that you are registering your RP or OP to a Federation Authority. + {% endblocktrans %} +

+
+ + +
+
+
+
+
+
+
+
+
+
+ +{% endblock container %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_registration.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_registration.html new file mode 100644 index 0000000..da31732 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_registration.html @@ -0,0 +1,59 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+

+ {% trans "Onboard your entity" %} +

+ +

+ {% blocktrans %} + Welcome to the example onboarding system of spid-cie-oidc-django. + In this page you can register a OIDC Federation entity to be onboarded in the federation. + + The form has some preliminar validations for your entity configuration, + once submitted, validated and tested, a Federation operator will enable your entity as a Descendant of the Federation Authority. + + {% endblocktrans%} +

+
+ {% csrf_token %} + {% for form_field in form %} +
+
+ + {% if form_field.field.widget.input_type == 'select'%} +
+ +
+ {% elif form_field.name == 'public_jwks' %} + + {% else %} + + {% endif %} +
+ {{ form_field.help_text }} + {{ form_field.errors }} +
+ {% endfor %} + + +
+ +
+{% endblock %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_resolve_statement.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_resolve_statement.html new file mode 100644 index 0000000..1826009 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_resolve_statement.html @@ -0,0 +1,59 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+ +

+ {% trans "Resolve Entity Statement" %} +

+ +

+ {% trans "By inserting the sub of an entity and the trust anchor you can resolve the statement of the entity" %} +

+ +
+ {% csrf_token %} + {% for form_field in form %} + + {% if form_field.field.widget.input_type == 'select'%} +
+ + {{ form_field.errors }} +
+ + {% else %} +
+
+ + +
+ {{ form_field.errors }} +
+ + {% endif %} + {% endfor %} + +
+
+ +
+
+ +
+
+{% endblock %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_schemas.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_schemas.html new file mode 100644 index 0000000..da14f84 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_schemas.html @@ -0,0 +1,34 @@ + +{% extends 'base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} + +
+
+

+ {% trans title %} +

+
+
+
+ {% for k, v in schemas.items %} +
+ +
+
+
+
{{ v | linebreaks}}
+
+
+ {% endfor %} +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_ec.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_ec.html new file mode 100644 index 0000000..dee3b41 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_ec.html @@ -0,0 +1,45 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} +
+

+ {% trans "Validate entity configuration" %} +

+ +

+ {% trans "enter a url of entity to check if it is compatible" %} +

+ +
+ {% csrf_token %} +
+
+ + + + +
+ +
+ {% if ec %} +
+
+ + +
+
+ {% endif %} + +
+
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_md.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_md.html new file mode 100644 index 0000000..b9dd053 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validate_md.html @@ -0,0 +1,33 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} +
+

+ {% blocktrans %} + {{title}} + {% endblocktrans %} +

+ +

+ {% trans description %} +

+ +
+ {% csrf_token %} +
+
+ + +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validating_tm.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validating_tm.html new file mode 100644 index 0000000..d6c7426 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/templates/onboarding_validating_tm.html @@ -0,0 +1,41 @@ + +{% extends 'base.html' %} +{% load i18n %} + +{% block centered_container %} + +
+

+ {% trans 'Validating Trust Mark' %} +

+ +

+ {% trans "To validate a trust mark insert the entity's sub and the id or directly the trust mark" %} +

+ +
+ {% csrf_token %} + {% for form_field in form %} +
+
+ + {% if form_field.name == 'trust_mark'%} + + {% else%} + + {% endif %} +
+ {{ form_field.errors }} +
+ + {% endfor %} + + +
+{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/mocked_responses.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/mocked_responses.py new file mode 100644 index 0000000..0e801a5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/mocked_responses.py @@ -0,0 +1,17 @@ +from django.test import Client + +import logging + +logger = logging.getLogger(__name__) + + +class OnboardingRegistrationResponse: + def __init__(self): + self.status_code = 200 + self.req_counter = 0 + self.client = Client() + self.result = None + + @property + def content(self): + self.req_counter += 1 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_01_onboarding.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_01_onboarding.py new file mode 100644 index 0000000..64e727d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_01_onboarding.py @@ -0,0 +1,111 @@ +from django.test import TestCase, Client, override_settings +from django.urls import reverse +import json + +from spid_cie_oidc.authority.tests.settings import * +from spid_cie_oidc.entity.models import * +from spid_cie_oidc.onboarding.urls import * +from spid_cie_oidc.entity.jwks import serialize_key +from spid_cie_oidc.entity.jwks import new_rsa_key +from spid_cie_oidc.entity.tests.settings import * + +from spid_cie_oidc.authority.models import * +from spid_cie_oidc.authority.tests.mocked_responses import * +from spid_cie_oidc.authority.tests.settings import * + +from spid_cie_oidc.onboarding.tests.mocked_responses import * + + +from unittest.mock import patch + + +class OnboardingTest(TestCase): + def setUp(self): + + self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf) + + self.data = { + "organization_name": "", + "url_entity": "", + "authn_buttons_page_url": "", + "type":"", + "public_jwks": "", + } + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.get", return_value=OnboardingRegistrationResponse()) + def test_onboarding_registration(self, mocked): + url = reverse("oidc_onboarding_registration") + client = Client() + res = client.get(url, self.data) + self.assertEqual(res.status_code, 200) + res = client.post(url, self.data) + self.assertFormError( + res, "form", "organization_name", "Enter your organization name" + ) + self.assertFormError(res, "form", "url_entity", + "Enter the entity unique identifier (HTTP URL)" + ) + self.assertFormError( + res, "form", "public_jwks", + "Enter the public JWKs published in your Entity Configuration, as double check." + ) + self.assertEqual(res.status_code, 200) + self.data["organization_name"] = "test name" + self.data["url_entity"] = "http://rp-test/oidc/rp" + self.data["type"] = "openid_relying_party" + self.data["authn_buttons_page_url"] = "https://authnurl.com" + self.data["public_jwks"] = {"key": "ciao"} + res = client.post(url, self.data) + self.assertContains(res, "Inserisci un JSON valido.") + self.assertEqual(res.status_code, 200) + jwk = serialize_key(new_rsa_key().pub_key) + self.data["public_jwks"] = json.dumps(jwk) + # res = client.post(url, self.data) + # self.assertEqual(res.status_code, 200) + # res = client.get(url) + # self.assertEqual(res.status_code, 200) + # self.assertIn(self.data["organization_name"], res.content.decode()) + # self.assertIn("acquired", res.content.decode()) + + def test_schema_authorization(self): + c = Client() + url = reverse("oidc_onboarding_schemas_Authorization") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas authorization endpoint", res.content.decode()) + + def test_schema_introspection(self): + c = Client() + url = reverse("oidc_onboarding_schemas_introspection") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas introspection endpoint", res.content.decode()) + + def test_schema_metadata(self): + c = Client() + url = reverse("oidc_onboarding_schemas_metadata") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas metadata Spid/Cie", res.content.decode()) + + def test_schema_revocation(self): + c = Client() + url = reverse("oidc_onboarding_schemas_revocation") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas revocation endpoint", res.content.decode()) + + def test_schema_token(self): + c = Client() + url = reverse("oidc_onboarding_schemas_token") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas token endpoint", res.content.decode()) + + def test_schema_client_assertion(self): + c = Client() + url = reverse("oidc_onboarding_schemas_jwt_client_assertion") + res = c.get(url) + self.assertTrue(res.status_code == 200) + self.assertIn("Schemas jwt", res.content.decode()) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_02_tools.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_02_tools.py new file mode 100644 index 0000000..4a2ea9f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_02_tools.py @@ -0,0 +1,252 @@ +import json +from django.test import TestCase, Client +from django.urls import reverse +from copy import deepcopy +from spid_cie_oidc.entity.jwtse import create_jws + +from spid_cie_oidc.onboarding.tests.tools_settings import jwk_priv, jwt, jwe +from spid_cie_oidc.entity.tests.settings import ta_conf_data +from spid_cie_oidc.authority.models import ( + FederationEntityConfiguration, + FederationEntityProfile, + FederationDescendant, + FederationEntityAssignedProfile +) +from spid_cie_oidc.authority.tests.settings import ( + rp_conf, + RP_PROFILE, + rp_onboarding_data +) + +from spid_cie_oidc.entity.jwks import ( + private_pem_from_jwk, + public_pem_from_jwk +) + +from spid_cie_oidc.entity.tests.op_metadata_settings import OP_METADATA_SPID +from spid_cie_oidc.entity.tests.rp_metadata_settings import RP_METADATA_CIE +from spid_cie_oidc.provider.tests.authn_request_settings import AUTHN_REQUEST_SPID +from spid_cie_oidc.authority.tests.settings import RP_METADATA_JWK1, RP_METADATA_JWK1_pub + +class ToolsTests(TestCase): + + def setUp(self): + + self.client = Client() + self.ta_conf = FederationEntityConfiguration.objects.create(**ta_conf_data) + self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf) + + self.rp_profile = FederationEntityProfile.objects.create(**RP_PROFILE) + self.rp = FederationDescendant.objects.create(**rp_onboarding_data) + + self.rp_assigned_profile = FederationEntityAssignedProfile.objects.create( + descendant=self.rp, profile=self.rp_profile, issuer=self.ta_conf + ) + + + def test_create_jwk(self): + url = reverse("oidc_onboarding_create_jwk") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + self.assertIsNotNone(res.context['private_jwk']) + + def test_convert_jwk_to_pem(self): + url = reverse("oidc_onboarding_convert_jwk") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + + #check with jwk error + res = self.client.post(url + '?type=private', {'kty': 'RSA'}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + #check with public jwk the function jwk priv to pem + res = self.client.post(url + '?type=private', {'jwk':json.dumps(RP_METADATA_JWK1_pub)}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + #check with right jwk private + res = self.client.post(url + '?type=private', {'jwk':json.dumps(RP_METADATA_JWK1)}) + self.assertEqual(res.status_code, 200) + self.assertNotIn("alert-error", res.content.decode()) + + #check with right jwk public + res = self.client.post(url + '?type=public', {'jwk':json.dumps(RP_METADATA_JWK1_pub)}) + self.assertEqual(res.status_code, 200) + self.assertNotIn("alert-error", res.content.decode()) + + def test_convert_pem_to_jwk(self): + url = reverse("oidc_onboarding_convert_pem") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + + pem_private = private_pem_from_jwk(RP_METADATA_JWK1) + res = self.client.post(url + '?type=private', {"pem": pem_private}) + self.assertEqual(res.status_code, 200) + self.assertNotIn("alert-error", res.content.decode()) + + pem_public = public_pem_from_jwk(RP_METADATA_JWK1_pub) + res = self.client.post(url + '?type=public', {"pem": pem_public}) + self.assertEqual(res.status_code, 200) + self.assertNotIn("alert-error", res.content.decode()) + + def test_decode_and_verify_jwt(self): + url = reverse("oidc_onboarding_tools_decode_jwt") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + res = self.client.post(url, {"jwt":jwt}) + self.assertEqual(res.status_code, 200) + self.assertIsNotNone(res.context['payload']) + self.assertIsNotNone(res.context['head']) + jwkError = {"jwkerror":"error"} + jwk = json.dumps(jwkError) + res = self.client.post(url, {"jwt":jwt, "jwk": jwk}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + jwk = json.dumps(jwk_priv) + res = self.client.post(url, {"jwt":jwt, "jwk": jwk}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + def test_decode_and_verify_jwe(self): + + # first failed because no jwks is submitted + url = reverse("oidc_onboarding_tools_decode_jwt") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + res = self.client.post(url, {"jwt":jwe}) + self.assertEqual(res.status_code, 400) + + res = self.client.post(url, {"jwt": jwe, "jwk": json.dumps(jwk_priv)}) + self.assertEqual(res.status_code, 200) + + def test_resolve_statement(self): + url = reverse("oidc_onboarding_resolve_statement") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + res = self.client.get(url, { + "sub":"http://127.0.0.1:8000/oidc", + "type": "openid_provider", + "trust_anchor": "http://127.0.0.1:8000" + }) + self.assertEqual(res.status_code, 200) + self.assertIn("text-danger", res.content.decode()) + # self.assertIn( + # "Failed to resolve entity statement, Please check your inserted data", + # res.content.decode()) + + form_data= { + "sub": "http://127.0.0.1:8000/oidc/op", + "type": "openid_provider", + "trust_anchor": "http://127.0.0.1:8000", + } + res = self.client.get(url, form_data) + self.assertEqual(res.status_code, 200) + self.assertIsNotNone(res.context['resolved_statement']) + + def test_validating_trust_mark(self): + url = reverse("oidc_onboarding_validating_trustmark") + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + + res = self.client.get(url, { + "id": "https://www.ciao.gov.it/certification/rp", + "sub": "http://ciao.it/oidc/rp/", + }) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + res = self.client.get(url, { + "id": "https://www.spid.gov.it/certification/rp", + "sub": "http://rp-test.it/oidc/rp/", + }) + + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + trust_mark = self.rp_assigned_profile.trust_mark_as_jws + + res = self.client.get(url, { + "trust_mark": trust_mark, + }) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + def test_validate_metadata(self): + url = reverse("oidc_onboarding_validate_md") + + OP_METADATA_SPID_WRONG = deepcopy(OP_METADATA_SPID) + OP_METADATA_SPID_WRONG["issuer"] = "htps://idserver.servizicie.interno.gov.it/op/" + res = self.client.post(url + '?metadata_type=op_metadata&provider_profile=spid', + {'md': json.dumps(OP_METADATA_SPID_WRONG)}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + res = self.client.post(url + '?metadata_type=op_metadata&provider_profile=spid', + {'md': json.dumps(OP_METADATA_SPID)}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + RP_METADATA_CIE_WRONG = deepcopy(RP_METADATA_CIE) + RP_METADATA_CIE_WRONG["jwks_uri"] = "htps://registry.cie.gov.it/keys.json" + res = self.client.post(url + '?metadata_type=rp_metadata&provider_profile=cie', + {'md': json.dumps(RP_METADATA_CIE_WRONG)}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + res = self.client.post(url + '?metadata_type=rp_metadata&provider_profile=cie', + {'md': json.dumps(RP_METADATA_CIE)}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + res = self.client.post(url + '?metadata_type=rp_metadata&provider_profile=cie', + {'md': "hello world"}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + def test_validating_trust_mark(self): + url = reverse("oidc_onboarding_validating_trustmark") + res = self.client.post(url) + self.assertEqual(res.status_code, 200) + + res = self.client.post(url, data={ + "id": "https://www.ciao.gov.it/certification/rp", + "sub": "http://ciao.it/oidc/rp/", + }) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + res = self.client.post(url, data={ + "id": "https://www.spid.gov.it/certification/rp", + "sub": "http://rp-test.it/oidc/rp/", + }) + + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + trust_mark = self.rp_assigned_profile.trust_mark_as_jws + + res = self.client.post(url, data={ + "trust_mark": trust_mark, + }) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + def test_validate_authn_request(self): + url = reverse("oidc_onboarding_validate_authn_request_jwt") + AUTHN_REQUEST_SPID_WRONG = deepcopy(AUTHN_REQUEST_SPID) + AUTHN_REQUEST_SPID_WRONG["client_id"] = "hps://rp.cie.it/callback1/" + jwt = create_jws(AUTHN_REQUEST_SPID_WRONG, RP_METADATA_JWK1) + res = self.client.post(url + '?provider_profile=spid', + {'md': jwt}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-error", res.content.decode()) + + jwt = create_jws(AUTHN_REQUEST_SPID, RP_METADATA_JWK1) + res = self.client.post(url + '?provider_profile=spid', + {'md': jwt}) + self.assertEqual(res.status_code, 200) + self.assertIn("alert-success", res.content.decode()) + + + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_11_admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_11_admin.py new file mode 100644 index 0000000..d902fdc --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/test_11_admin.py @@ -0,0 +1,25 @@ + +from django.test import TestCase +from spid_cie_oidc.authority.models import FederationDescendant, FederationDescendantContact + +from spid_cie_oidc.onboarding.admin import OnBoardingRegistrationAdmin +from spid_cie_oidc.onboarding.models import OnBoardingRegistration + +from spid_cie_oidc.authority.tests.settings import RP_METADATA_JWK1_pub, rp_conf +class AdminTest(TestCase): + + def setUp(self): + self.onb_regist = OnBoardingRegistration.objects.create( + organization_name = "RP test", + url_entity = rp_conf["sub"], + public_jwks = RP_METADATA_JWK1_pub, + type = "openid_relying_party", + contact = "ops@rp.example.it" + ) + print(self.onb_regist) + + def test_make_published(self): + queryset = [self.onb_regist] + OnBoardingRegistrationAdmin.enable_as_descendant(None, request = {}, queryset = queryset) + self.assertTrue(len(FederationDescendantContact.objects.all()) == 1) + self.assertTrue(len(FederationDescendant.objects.all()) == 1) \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/tools_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/tools_settings.py new file mode 100644 index 0000000..928bed0 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/tests/tools_settings.py @@ -0,0 +1,15 @@ +from spid_cie_oidc.entity.jwtse import create_jwe + + +jwk_priv = { + 'kty': 'RSA', + 'n': 'wct3JUvUxk0JYnDD0sdy0BkLfiLO_Kfy7hvRp7AC1GlvV74FrDfDnYeTTCDPoXEJN2DMiTnMvWXENaFcBkI1RMSaMqupwp6aoGniFDNCiwEh5wgrjVpaxb5Au4pj4Lq1ecbRDDktwqKDVoQsCXsAAApiTkwjq4g6liH8-PTVG-QCM0gL7VIU0jEKCGX6BuZwuKvfEeNTMboMLnn8_AqdsTtvmDommLaOiOgjU0qS7cvowdVX7fhtDeCgVYvPetAFEvimPAE0i4cQ9RAoqlgFCBpRXz9UyeGAXlaFWtLk_t59jNv4nJEP1gQ2VvAvqA4gOn0f9s7Gf1AsFSxrnmeP1Q', + 'e': 'AQAB', + 'd': 'vtW1hiyeYogJ7b5Nl4qALRv8jUGJwpNATelnE1Hn93O8CRM-UtC2-LFBA35r9_ZvtPDFeXxJeVh1VrqG_ELGYemYTr1sdhHpAn5B-tJffLKox_Drsk5_hKpM5GDq6anyevClevvza-7Mlq2hD9TnsyKGm8OufIOx7ZWany25jbPK_ksU9X_G9J5RkQ5aDUz7OiUJ-LyL2xmW4gldp_frk_lYgFTgWOp7ZilDQWKYkeLCDzuuRLbM01D7jEJR2c28huQ_vVlw7457NVh-WWyY8Kw0TBgMOU8rrfmgdicv-kbrQaAz60ip95g5FV8ALqSewQYo3v7-4FD8nWVWu01yIQ', + 'p': '5Qo4l7oom0P08IBun83TtYOEoDjFEuxsOKcWHfSMYcbMpMCog97BA7ji_0tFHVgqjUAfnEXj4U1yi-ScKZHeiusVYzo5_tQXm8f9bjylw4kOQ1UMSoloz7_sd9xT9jpnWXNxLWHXCoXXWDHUV-iXTewDnRM3McWkA1REIdGogG8', + 'q': '2JsvLNvn5O1CAm5bZesbI4WA2Lx30Y0pEfeqMgpsGYg6QSP-zHc1-jJ5D2zhl4W334NeeUx5pSFzQczg7k-jCWrgKD-nvHzjVxOrzKsG4bOUHIqMDaRbeYH9t-BFNy3b8E6O6sqHKWTbd36JFOi7OBZpmtwSVl7JRYr7sXkUDfs', + 'kid': 'yxMflR9L6k9Uf-uarpBS3OfgcO1SzbwpZEWbCeOSM3I' +} + +jwt = 'eyJhbGciOiJSUzI1NiIsImtpZCI6Inl4TWZsUjlMNms5VWYtdWFycEJTM09mZ2NPMVN6YndwWkVXYkNlT1NNM0kifQ.eyJjbGllbnRfaWQiOiJodHRwczovL3JwLmNpZS5pdCIsInJlc3BvbnNlX3R5cGUiOiJjb2RlIiwic2NvcGUiOiJvcGVuaWQiLCJjb2RlX2NoYWxsZW5nZSI6InFXSmxNZTB4ZGJYckt4VG03MkVwSDY1OWJVeEF4dzgwIiwiY29kZV9jaGFsbGVuZ2VfbWV0aG9kIjoiUzI1NiIsIm5vbmNlIjoiTUJ6R3F5ZjlReXREMjhldXB5V2hTcU1qNzhXTnFwYzIiLCJwcm9tcHQiOiJjb25zZW50IGxvZ2luIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6Ly9ycC5jaWUuaXQvY2FsbGJhY2sxLyIsImFjcl92YWx1ZXMiOiJDSUVfTDEgQ0lFX0wyIiwiY2xhaW1zIjp7ImlkX3Rva2VuIjp7ImZhbWlseV9uYW1lIjp7ImVzc2VudGlhbCI6dHJ1ZX0sImVtYWlsIjp7ImVzc2VudGlhbCI6dHJ1ZX19LCJ1c2VyaW5mbyI6eyJuYW1lIjpudWxsLCJmYW1pbHlfbmFtZSI6bnVsbH19LCJzdGF0ZSI6ImZ5WmlPTDlMZjJDZUt1TlQySnp4aUxSRGluazB1UGNkIn0.tJqM9mXrg35C78WkCTJQgr7cDirr5mBa22XHDKQI4Bsto5g70LnBJcBN4v85gAqTOYOvhVPcjfN5UKtS2yTnlXMMbWtIjjX3lBlPVTxze5CjdPVKaDf4EB_za2ampPmc2pZh71T9M9exUaSSi1537Sxq9Wbh3QHomtNOoCapp9IS5UheBDe7P3xkU5-pm24NtC3M9EdJHa8AS42EpdbYx5N4md2DaUg0mU1-5sBFLoESVXOj6J1qDQdOqs8Z-wFpjpBi82v4l7woWalzkdVsmeTDxkeb2TCuNAjN29rpgYiVw78WMFi2P5c7t2ZPdQADUdAqpbUvfN4k1hq3Q-WPPA' +jwe = create_jwe(jwt, jwk_priv) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/urls.py new file mode 100644 index 0000000..fb2188e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/urls.py @@ -0,0 +1,145 @@ +"""acsia_agent_proj URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/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.urls import path + +from .views import ( + onboarding_landing, + onboarding_registration, + onboarding_entities, + onboarding_create_jwk, + onboarding_convert_jwk, + onboarding_convert_pem, + onboarding_resolve_statement, + onboarding_schemas_federation_entity_endpoints, + onboarding_validating_trustmark, + onboarding_decode_jwt, + onboarding_apply_policy, + onboarding_validate_md, + onboarding_schemas_authorization, + onboarding_schemas_introspection, + onboarding_schemas_metadata, + onboarding_schemas_revocation, + onboarding_schemas_token, + onboarding_schemas_jwt_client_assertion, + onboarding_validate_authn_request, + onboarding_validate_ec, +) + +_PREF = getattr(settings, "OIDC_PREFIX", "") + +urlpatterns = [ + path( + f"{_PREF}onboarding/landing", + onboarding_landing, + name="oidc_onboarding_landing", + ), + path( + f"{_PREF}onboarding/registration", + onboarding_registration, + name="oidc_onboarding_registration", + ), + path( + f"{_PREF}onboarding/entities", + onboarding_entities, + name="oidc_onboarding_entities", + ), + path( + f"{_PREF}onboarding/tools/create-jwk", + onboarding_create_jwk, + name="oidc_onboarding_create_jwk", + ), + path( + f"{_PREF}onboarding/tools/convert-jwk", + onboarding_convert_jwk, + name="oidc_onboarding_convert_jwk", + ), + path( + f"{_PREF}onboarding/convert-pem", + onboarding_convert_pem, + name="oidc_onboarding_convert_pem", + ), + path( + f"{_PREF}onboarding/tools/resolve-statement", + onboarding_resolve_statement, + name="oidc_onboarding_resolve_statement", + ), + path( + f"{_PREF}onboarding/tools/validating-trustmark", + onboarding_validating_trustmark, + name="oidc_onboarding_validating_trustmark", + ), + path( + f"{_PREF}onboarding/tools/decode-jwt", + onboarding_decode_jwt, + name="oidc_onboarding_tools_decode_jwt", + ), + path( + f"{_PREF}onboarding/tools/apply-policy", + onboarding_apply_policy, + name="oidc_onboarding_tools_apply_policy", + ), + path( + f"{_PREF}onboarding/schemas/federation_entity", + onboarding_schemas_federation_entity_endpoints, + name="oidc_onboarding_schemas_federation_entity", + ), + path( + f"{_PREF}onboarding/schemas/authorization", + onboarding_schemas_authorization, + name="oidc_onboarding_schemas_Authorization", + ), + path( + f"{_PREF}onboarding/schemas/introspection", + onboarding_schemas_introspection, + name="oidc_onboarding_schemas_introspection", + ), + path( + f"{_PREF}onboarding/schemas/metadata", + onboarding_schemas_metadata, + name="oidc_onboarding_schemas_metadata", + ), + path( + f"{_PREF}onboarding/schemas/revocation", + onboarding_schemas_revocation, + name="oidc_onboarding_schemas_revocation", + ), + path( + f"{_PREF}onboarding/schemas/token", + onboarding_schemas_token, + name="oidc_onboarding_schemas_token", + ), + path( + f"{_PREF}onboarding/schemas/jwt/client/assertion", + onboarding_schemas_jwt_client_assertion, + name="oidc_onboarding_schemas_jwt_client_assertion", + ), + path( + f"{_PREF}onboarding/tools/validate-md", + onboarding_validate_md, + name="oidc_onboarding_validate_md", + ), + path( + f"{_PREF}onboarding/tools/validate-authn-request", + onboarding_validate_authn_request, + name="oidc_onboarding_validate_authn_request_jwt", + ), + path( + f"{_PREF}onboarding/tools/validate-ec", + onboarding_validate_ec, + name="oidc_onboarding_validate_ec", + ), +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/validators.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/validators.py new file mode 100644 index 0000000..28a370c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/validators.py @@ -0,0 +1,12 @@ +from .models import OnBoardingRegistration +from django.core.exceptions import ValidationError + + +def unique_entity_url(value): # pragma: no cover + if OnBoardingRegistration.objects.filter(url_entity=value): + raise ValidationError(f"{value} already onboarded") + + +def trust_anchor_validator(value): # pragma: no cover + pass + # raise ValidationError(f"{value} trust anchor not exist") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/views.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/views.py new file mode 100644 index 0000000..ea3f638 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/views.py @@ -0,0 +1,458 @@ +import json +from django.core.paginator import Paginator +from django.shortcuts import render, redirect +from django.contrib import messages +from django.utils.translation import gettext as _ + +from spid_cie_oidc.authority.schemas.fetch_endpoint_request import FetchRequest +from spid_cie_oidc.authority.schemas.list_endpoint import ListRequest, ListResponse +from spid_cie_oidc.entity.schemas.resolve_endpoint import ResolveRequest, ResolveResponse +from spid_cie_oidc.authority.schemas.trust_mark_status_endpoint import TrustMarkRequest, TrustMarkResponse +from spid_cie_oidc.entity.schemas.fa_metadata import FAMetadata + + +from .forms import ( + OnboardingRegistrationForm, + OnboardingCreateTrustChain, + OnboardingValidatingTrustMarkForm, + OnboardingDecodeForm +) +from .models import OnBoardingRegistration +from spid_cie_oidc.entity.jwks import ( + private_pem_from_jwk, + public_pem_from_jwk, + new_rsa_key, + serialize_key, + private_jwk_from_pem, + public_jwk_from_pem +) + +from spid_cie_oidc.entity.jwtse import unpad_jwt_head, unpad_jwt_payload, verify_jws, decrypt_jwe +from spid_cie_oidc.authority.views import trust_mark_status +from spid_cie_oidc.authority.validators import validate_entity_configuration +from spid_cie_oidc.provider.schemas.authn_requests import AuthenticationRequestSpid +from spid_cie_oidc.provider.schemas.authn_response import AuthenticationResponse +from spid_cie_oidc.provider.schemas.authn_response import AuthenticationErrorResponse +from spid_cie_oidc.provider.schemas.introspection_request import IntrospectionRequest +from spid_cie_oidc.provider.schemas.introspection_response import IntrospectionResponse +from spid_cie_oidc.provider.schemas.introspection_response import IntrospectionErrorResponseSpid +from spid_cie_oidc.entity.schemas.op_metadata import OPMetadataSpid, OPMetadataCie +from spid_cie_oidc.entity.schemas.rp_metadata import RPMetadataSpid,RPMetadataCie +from spid_cie_oidc.provider.schemas.revocation_request import RevocationRequest +from spid_cie_oidc.provider.schemas.token_requests import TokenAuthnCodeRequest +from spid_cie_oidc.provider.schemas.token_response import TokenResponse +from spid_cie_oidc.provider.schemas.token_response import TokenErrorResponse + +from spid_cie_oidc.provider.schemas.token_requests import TokenRefreshRequest +from spid_cie_oidc.provider.schemas.token_response import TokenRefreshResponse + +from spid_cie_oidc.provider.schemas.jwt import JwtStructure +from spid_cie_oidc.entity.policy import apply_policy + +from spid_cie_oidc.relying_party.settings import RP_PROVIDER_PROFILES +from spid_cie_oidc.provider.settings import OIDCFED_PROVIDER_PROFILES +from spid_cie_oidc.entity.views import resolve_entity_statement + + +def onboarding_landing(request): # pragma: no cover + return render(request, "onboarding_landing.html") + + +def onboarding_registration(request): + form = OnboardingRegistrationForm() + context = {"form": form} + if request.method == "POST": + form = OnboardingRegistrationForm(request.POST) + if not form.is_valid(): + context = {"form": form} + else: + form_dict = {**form.cleaned_data} + OnBoardingRegistration.objects.create(**form_dict) + messages.success(request, _("Registration successfully")) + return redirect("oidc_onboarding_entities") + return render(request, "onboarding_registration.html", context) + + +def onboarding_entities(request): + entity_list = OnBoardingRegistration.objects.all() + p = Paginator(entity_list, 10) + page = request.GET.get("page") + entities = p.get_page(page) + return render( + request, + "onboarding_entities.html", + {"entity_list": entity_list, "entities": entities}, + ) + + +def onboarding_create_jwk(request): + _rsa_key = new_rsa_key() + private_jwk = serialize_key(_rsa_key.priv_key, 'private') + public_jwk = serialize_key(_rsa_key.pub_key) + context = { + "private_jwk": json.dumps(private_jwk, indent=4), + "public_jwk": json.dumps(public_jwk, indent=4) + } + return render(request, 'onboarding_jwk.html', context) + + +def onboarding_convert_jwk(request): + jwk_type = request.GET.get('type') + context = { + "jwk": "", + "pem": "", + "jwk_type": jwk_type + } + if request.method == 'POST': + try: + jwk_str = request.POST.get('jwk') + jwk_str_double_quote = jwk_str.replace("'", '"') + jwk_dict = json.loads(jwk_str_double_quote) + + if jwk_type == 'private': + pem = private_pem_from_jwk(jwk_dict) + if jwk_type == 'public': + pem = public_pem_from_jwk(jwk_dict) + + context = { + "jwk": jwk_dict, + "pem": pem, + "jwk_type": jwk_type + } + except Exception as e: + messages.error(request, _(f" {e} ")) + return render(request, 'onboarding_convert_jwk.html', context) + + +def onboarding_convert_pem(request): + pem_type = request.GET.get('type') + context = { + "pem_type": pem_type + } + if request.method == 'POST': + try: + pem = request.POST.get('pem') + if pem_type == 'private': + jwk = private_jwk_from_pem(pem) + if pem_type == 'public': + jwk = public_jwk_from_pem(pem) + context = { + "pem": pem, + "pem_type": pem_type, + "jwk": jwk + } + except Exception as e: + messages.error(request, _(f" {e} ")) + return render(request, 'onboarding_convert_pem.html', context) + + +def onboarding_resolve_statement(request): + if "sub" in request.GET : + form = OnboardingCreateTrustChain(request.GET) + else: + form = OnboardingCreateTrustChain() + context = {'form': form} + if form.is_valid(): + context = { + 'form': form, + "resolved_statement": "", + } + try: + res = resolve_entity_statement(request, format="json") + resultJson = json.loads(res.content.decode()) + context["resolved_statement"] = json.dumps(resultJson, indent=4) + except Exception: + messages.error(request, _('Failed to resolve entity statement, Please check your inserted data')) + + return render(request, 'onboarding_resolve_statement.html', context) + + +def onboarding_validating_trustmark(request): + if "id" in request.POST or "trust_mark" in request.POST: + form = OnboardingValidatingTrustMarkForm(request.POST) + else: + form = OnboardingValidatingTrustMarkForm() + + context = {"form": form} + + if form.is_valid(): + res = trust_mark_status(request) + body = res.content.decode() + context = {'form': form} + ct = res.get("Content-Type", "") + if res.status_code == 200 and ct.startswith("application/trust-mark-status-response+jwt"): + # Draft 48: response is a signed JWT with status claim + from spid_cie_oidc.entity.jwtse import unpad_jwt_payload + content = unpad_jwt_payload(body) + active = content.get("status") == "active" + else: + # Error response (e.g. 404) or legacy JSON body + try: + content = json.loads(body) + active = content.get("active", False) + except (json.JSONDecodeError, TypeError): + active = False + if active: + messages.success(request, _('Validation Trust Mark Successfully')) + else: + messages.error(request, _('Validation Trust Mark Failed')) + return render(request, 'onboarding_validating_tm.html', context) + + +def onboarding_validate_md(request): + metadata_type = request.GET.get('metadata_type') + provider_profile = request.GET.get('provider_profile') + md_type = metadata_type.replace("_", " ") + title = f'Validate {md_type} {provider_profile}' + description = f'Enter a {md_type} of {provider_profile} to verify if it is compatible' + context = { + "metadata_type": metadata_type, + "provider_profile": provider_profile, + "title": title, + "description": description, + "field_name":"metadata" + } + if request.POST.get('md'): + md = request.POST['md'] + context["md"] = md + try: + md_str_double_quote = md.replace("'", '"') + metadata = json.loads(md_str_double_quote) + if metadata_type == 'op_metadata': + schema = OIDCFED_PROVIDER_PROFILES[provider_profile] + schema[metadata_type](**metadata) + messages.success(request, _('Validation Metadata Successfully')) + if metadata_type == 'rp_metadata': + schema = RP_PROVIDER_PROFILES[provider_profile] + schema[metadata_type](**metadata) + messages.success(request, _('Validation Metadata Successfully')) + except Exception as e: + messages.error(request, f"Validation Failed: {e}") + return render(request, 'onboarding_validate_md.html', context) + + +def onboarding_validate_authn_request(request): + provider_profile = request.GET.get('provider_profile') + title = f'Validate authn request {provider_profile}' + description = f'Enter a jwt of authn request for {provider_profile} to verify if it is compatible' + context = { + "provider_profile": provider_profile, + "title": title, + "description": description, + "field_name":"jwt" + } + if request.POST.get('md'): + jwt_str = request.POST['md'] + context = { + "provider_profile": provider_profile, + "title": title, + "description": description, + "field_name":"jwt", + "md": jwt_str + } + payload = unpad_jwt_payload(jwt_str) + schema = OIDCFED_PROVIDER_PROFILES[provider_profile] + try: + schema["authorization_request"](**payload) + messages.success(request, _('Validation Authn Request Successfully')) + except Exception as e: + messages.error(request, f"Validation Failed: {e}") + return render(request, 'onboarding_validate_md.html', context) + + +def onboarding_validate_ec(request): + context = {} + if request.POST: + url = request.POST.get("url") + context = {"url": url} + try: + ec = validate_entity_configuration(url) + context["ec"] = json.dumps(ec.payload, indent=4) + messages.success(request, _('Validation Entity Configuration Successfully')) + except Exception as e : + messages.error(request, f"Validation Failed: {e}") + return render(request, 'onboarding_validate_ec.html', context) + return render(request, 'onboarding_validate_ec.html', context) + + +def onboarding_decode_jwt(request): + if request.method == "GET": + form = OnboardingDecodeForm() + else: + form = OnboardingDecodeForm(request.POST) + + context = {'form': form} + + if form.is_valid(): + form_dict = {**form.cleaned_data} + jwt = form_dict['jwt'] + try: + head = unpad_jwt_head(jwt) + except Exception as e: + messages.error( + request, + f"JWS verification failed due to missing JWT header: {e}" + ) + return render(request, "onboarding_decode_jwt.html", context) + + if head.get('enc', None): + if not form_dict.get('jwk'): + messages.error( + request, + "JWE needs a private jwk to be decrypted" + ) + return render( + request, "onboarding_decode_jwt.html", context, status = 400 + ) + # JWE here + jwk = form_dict['jwk'] + jwt = decrypt_jwe(jwt, jwk) + + try: + + payload = unpad_jwt_payload(jwt) + context["head"] = json.dumps(head, indent=4) + context["payload"] = json.dumps(payload, indent=4) + jwk = form_dict["jwk"] + if jwk: + verify_jws(jwt, jwk) + messages.success(request, _('Your jws is verified')) + except Exception as e: + messages.error(request, f"JWS verification failed: {e}") + + return render(request, "onboarding_decode_jwt.html", context) + + +def onboarding_apply_policy(request): + context = { + "md": "", + "policy": "", + "result": "" + } + if request.GET.get('md') and request.GET.get('policy'): + context = { + "md": request.GET['md'], + "policy": request.GET['policy'], + "result": "" + } + try: + md = json.loads(request.GET['md']) + policy = json.loads(request.GET['policy']) + reuslt = apply_policy(md, policy) + context["result"] = json.dumps(reuslt, indent=4) + except Exception as e: + messages.error(request, {e}) + return render(request, 'onboarding_apply_policy.html', context) + + +def onboarding_schemas_authorization(request): + auth_request = AuthenticationRequestSpid.schema_json(indent=2) + auth_res_succ = AuthenticationResponse.schema_json(indent=2) + auth_res_err = AuthenticationErrorResponse.schema_json(indent=2) + content = { + "title": "Schemas authorization endpoint", + "schemas": { + "authorization request": auth_request, + "authorization successful response": auth_res_succ, + "authorization error response": auth_res_err, + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_introspection(request): + intro_request = IntrospectionRequest.schema_json(indent=2) + intro_res_succ = IntrospectionResponse.schema_json(indent=2) + intro_res_err = IntrospectionErrorResponseSpid.schema_json(indent=2) + content = { + "title": "Schemas introspection endpoint", + "schemas": { + "introspection request": intro_request, + "introspection successful response": intro_res_succ, + "introspection error response": intro_res_err, + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_federation_entity_endpoints(request): + federation_entity_fetch_request = FetchRequest.schema_json(indent=2) + federation_entity_list_request = ListRequest.schema_json(indent=2) + federation_entity_list_response = ListResponse.schema_json(indent=2) + federation_entity_resolve_request = ResolveRequest.schema_json(indent=2) + federation_entity_resolve_response = ResolveResponse.schema_json(indent=2) + federation_entity_status_request = TrustMarkRequest.schema_json(indent=2) + federation_entity_status_response = TrustMarkResponse.schema_json(indent=2) + content = { + "title": "Schemas Federation entity endpoint", + "schemas": { + "Federation entity fetch": federation_entity_fetch_request, + "Federation entity list request": federation_entity_list_request, + "Federation entity list response": federation_entity_list_response, + "Federation entity resolve request": federation_entity_resolve_request, + "Federation entity resolve response": federation_entity_resolve_response, + "Federation entity status request": federation_entity_status_request, + "Federation entity status response": federation_entity_status_response + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_metadata(request): + op_meta_spid = OPMetadataSpid.schema_json(indent=2) + op_meta_cie = OPMetadataCie.schema_json(indent=2) + rp_meta_spid = RPMetadataSpid.schema_json(indent=2) + rp_meta_cie = RPMetadataCie.schema_json(indent=2) + fed_entity = FAMetadata.schema_json(indent=2) + content = { + "title": "Schemas metadata Spid/Cie", + "schemas": { + "Federation entity": fed_entity, + "OP Spid": op_meta_spid, + "OP Cie": op_meta_cie, + "RP Spid": rp_meta_spid, + "RP Cie": rp_meta_cie + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_revocation(request): + revoc_request = RevocationRequest.schema_json(indent=2) + content = { + "title": "Schemas revocation endpoint", + "schemas": { + "revocation request": revoc_request + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_token(request): + auth_code_req = TokenAuthnCodeRequest.schema_json(indent=2) + auth_code_res = TokenResponse.schema_json(indent=2) + refr_req = TokenRefreshRequest.schema_json(indent=2) + refr_res = TokenRefreshResponse.schema_json(indent=2) + err_res = TokenErrorResponse.schema_json(indent=2) + content = { + "title": "Schemas token endpoint", + "schemas": { + "token authn code request": auth_code_req, + "token authn code response": auth_code_res, + "refresh token request": refr_req, + "refresh token response": refr_res, + "token error response": err_res + } + } + return render(request, "onboarding_schemas.html", content) + + +def onboarding_schemas_jwt_client_assertion(request): + jwt_client_asser = JwtStructure.schema_json(indent=2) + content = { + "title": "Schemas jwt", + "schemas": { + "jwt client assertion": jwt_client_asser + } + } + return render(request, "onboarding_schemas.html", content) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/admin.py new file mode 100644 index 0000000..e29b386 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/admin.py @@ -0,0 +1,36 @@ +from django.contrib import admin + +from .models import IssuedToken, OidcSession + + +@admin.register(OidcSession) +class OidcSessionAdmin(admin.ModelAdmin): + list_display = ("user_uid", "client_id", "created", "revoked") + list_filter = ("created", "revoked") + search_fields = ("client_id", "user_uid") + readonly_fields = ( + "nonce", + "auth_code", + "user_uid", + "user", + "sid", + "client_id", + "created", + "revoked", + "user_uid", + "authz_request", + ) + + +@admin.register(IssuedToken) +class IssuedTokenAdmin(admin.ModelAdmin): + list_display = ("client_id", "user_uid", "created") + list_filter = ("created",) + search_fields = ("session__user_uid", "session__client_id") + readonly_fields = ( + "access_token", + "id_token", + "refresh_token", + "created", + "session", + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/apps.py new file mode 100644 index 0000000..a240807 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class SpidCieOidcOpConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.provider" + verbose_name = "SPID/CIE OIDC Federation Provider" + label = "spid_cie_oidc_provider" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/exceptions.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/exceptions.py new file mode 100644 index 0000000..c3a3a77 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/exceptions.py @@ -0,0 +1,22 @@ +class AuthzRequestReplay(Exception): + pass + + +class InvalidSession(Exception): + pass + + +class RevokedSession(Exception): + pass + + +class ValidationException(Exception): + pass + + +class ExpiredAuthCode(Exception): + pass + + +class InvalidRefreshRequestException(Exception): + pass diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/forms.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/forms.py new file mode 100644 index 0000000..8773f6a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/forms.py @@ -0,0 +1,38 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class AuthLoginForm(forms.Form): + """ + The User Authentication Form for an Authorized request + """ + + username = forms.CharField( + initial="", + label=_("Username"), + error_messages={"required": _("Enter your username")}, + ) + password = forms.CharField( + initial="", + label=_("Password"), + error_messages={"required": _("Enter your password")}, + widget=forms.PasswordInput(), + ) + # when a prue SSO will be enabled. + # forget_agreement = forms.BooleanField(label=_("Delete previous agreement"), + # required=False, + # localize=True) + # forget_login = forms.BooleanField(label=_("Forget access"), + # required=False, + # localize=True) + + +class AuthzHiddenForm(forms.Form): + """ + A hidden form to carry the original authz request + """ + authz_request_object = forms.CharField(widget=forms.HiddenInput()) + + +class ConsentPageForm(forms.Form): + agree = forms.BooleanField(initial=True, widget=forms.HiddenInput()) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/fetch_openid_relying_parties.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/fetch_openid_relying_parties.py new file mode 100644 index 0000000..9394d76 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/management/commands/fetch_openid_relying_parties.py @@ -0,0 +1,106 @@ +import json +import logging + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.utils.translation import gettext as _ + +from spid_cie_oidc.entity.statements import ( + get_entity_configurations, + get_http_url, + EntityConfiguration, +) +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain + + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Attribute release query" + + def add_arguments(self, parser): + parser.epilog = "Example: ./manage.py fetch_openid_providers" + parser.add_argument( + "--from", + nargs="*", + default=[], + help=_( + "A list of url of authorities, separated by space where " + "download the list of OPs" + ), + ) + parser.add_argument( + "-f", + "--force", + action="store_true", + required=False, + help=_("Don't use already cached statements and chains"), + ) + parser.add_argument( + "-debug", required=False, action="store_true", help="see debug message" + ) + + def handle(self, *args, **options): + if not options["from"]: + return # pragma: no cover + + res = [] + rp_subs = [] + jwts = get_entity_configurations(options["from"]) + auth_ecs = [] + for i in jwts: + ec = EntityConfiguration(i) + if ec.validate_by_itself(): + auth_ecs.append(ec) + + if not auth_ecs: + logger.warning( + f"Any of {options['from']} has a valid EntityConfiguration " + "to get a List endpoint" + ) + return + + logger.info( + f"Found {auth_ecs} federation entities where to query a list of RPs" + ) + + list_urls = [] + for i in auth_ecs: + endpoint = ( + i.payload.get("metadata", {}) + .get("federation_entity", {}) + .get("federation_list_endpoint", "") + ) + if endpoint: + list_urls.append(f"{endpoint}?type=openid_relying_party") + + rp_subs = [] + for rp_sub in get_http_url(list_urls, httpc_params=settings.HTTPC_PARAMS): + + try: + urls = json.loads(rp_sub) + rp_subs.extend(urls) + except Exception as e: + logger.error(f"Failed {e}") + continue + + for rp_sub in rp_subs: + try: + tc = get_or_create_trust_chain( + subject=rp_sub, + trust_anchor=settings.OIDCFED_DEFAULT_TRUST_ANCHOR, + metadata__relying_party__is_null=False, + httpc_params=settings.HTTPC_PARAMS, + required_trust_marks=getattr( + settings, "OIDCFED_REQUIRED_TRUST_MARKS", [] + ), + force=options["force"], + ) + if tc.is_valid: + res.append(tc) + + logger.info(f"Final Metadata for {tc.sub}:\n\n{tc.metadata}") + + except Exception as e: + logger.exception(f"Failed to download {rp_sub} due to: {e}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0001_initial.py new file mode 100644 index 0000000..e52fb16 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0001_initial.py @@ -0,0 +1,84 @@ +# Generated by Django 4.0.2 on 2022-03-02 22:40 + +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="OidcSession", + fields=[ + ( + "id", + models.BigAutoField( + 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)), + ("client_id", models.URLField(blank=True, null=True)), + ("nonce", models.CharField(max_length=2048)), + ("authz_request", models.JSONField(max_length=2048)), + ("sub", models.CharField(blank=True, max_length=254, null=True)), + ("user_claims", models.JSONField(blank=True, default=dict)), + ("revoked", models.BooleanField(default=False)), + ("auth_code", models.CharField(max_length=2048)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "User Session", + "verbose_name_plural": "User Sessions", + "unique_together": {("client_id", "nonce")}, + }, + ), + migrations.CreateModel( + name="IssuedToken", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("access_token", models.TextField(blank=True, null=True)), + ("id_token", models.TextField(blank=True, null=True)), + ("refresh_token", models.TextField(blank=True, null=True)), + ( + "session", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_provider.oidcsession", + ), + ), + ], + options={ + 'ordering': ['-created'], + "verbose_name": "Issued Token", + "verbose_name_plural": "Issued Tokens", + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0002_remove_oidcsession_user_claims.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0002_remove_oidcsession_user_claims.py new file mode 100644 index 0000000..682a064 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0002_remove_oidcsession_user_claims.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.2 on 2022-03-02 23:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_provider", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="oidcsession", + name="user_claims", + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0003_remove_oidcsession_sub_issuedtoken_expires_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0003_remove_oidcsession_sub_issuedtoken_expires_and_more.py new file mode 100644 index 0000000..ccc68d6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0003_remove_oidcsession_sub_issuedtoken_expires_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.2 on 2022-03-05 01:18 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_provider", "0002_remove_oidcsession_user_claims"), + ] + + operations = [ + migrations.RemoveField( + model_name="oidcsession", + name="sub", + ), + migrations.AddField( + model_name="issuedtoken", + name="expires", + field=models.DateTimeField(default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="issuedtoken", + name="revoked", + field=models.BooleanField(default=False), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0004_alter_oidcsession_user.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0004_alter_oidcsession_user.py new file mode 100644 index 0000000..c8a846a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0004_alter_oidcsession_user.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.2 on 2022-03-06 23:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('spid_cie_oidc_provider', '0003_remove_oidcsession_sub_issuedtoken_expires_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oidcsession', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0005_oidcsession_sid.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0005_oidcsession_sid.py new file mode 100644 index 0000000..ff5ab00 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0005_oidcsession_sid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-03-07 00:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_provider', '0004_alter_oidcsession_user'), + ] + + operations = [ + migrations.AddField( + model_name='oidcsession', + name='sid', + field=models.CharField(blank=True, help_text='django session key', max_length=1024, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0006_oidcsession_acr.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0006_oidcsession_acr.py new file mode 100644 index 0000000..93dd1e3 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0006_oidcsession_acr.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-16 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_provider', '0005_oidcsession_sid'), + ] + + operations = [ + migrations.AddField( + model_name='oidcsession', + name='acr', + field=models.CharField(default='https://www.spid.gov.it/SpidL2', max_length=1024), + preserve_default=False, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0007_alter_issuedtoken_options_alter_oidcsession_options.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0007_alter_issuedtoken_options_alter_oidcsession_options.py new file mode 100644 index 0000000..e1e1507 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0007_alter_issuedtoken_options_alter_oidcsession_options.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-03-19 17:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_provider', '0006_oidcsession_acr'), + ] + + operations = [ + migrations.AlterModelOptions( + name='issuedtoken', + options={'verbose_name': 'Issued Token', 'verbose_name_plural': 'Issued Tokens'}, + ), + migrations.AlterModelOptions( + name='oidcsession', + options={'ordering': ['-created'], 'verbose_name': 'User Session', 'verbose_name_plural': 'User Sessions'}, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0008_alter_oidcsession_authz_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0008_alter_oidcsession_authz_request.py new file mode 100644 index 0000000..0217d88 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/0008_alter_oidcsession_authz_request.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.3 on 2023-12-13 14:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "spid_cie_oidc_provider", + "0007_alter_issuedtoken_options_alter_oidcsession_options", + ), + ] + + operations = [ + migrations.AlterField( + model_name="oidcsession", + name="authz_request", + field=models.JSONField(max_length=65536), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/models.py new file mode 100644 index 0000000..f4d60ed --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/models.py @@ -0,0 +1,107 @@ +import hashlib +import logging + +from django.contrib.auth import get_user_model +from django.contrib.sessions.models import Session +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext as _ +from spid_cie_oidc.entity.abstract_models import TimeStampedModel +from spid_cie_oidc.provider.settings import OIDCFED_PROVIDER_SALT + +logger = logging.getLogger(__name__) + + +class OidcSession(TimeStampedModel): + """ + Store UserSessionInfo, ClientSessionInfo and Grant + """ + + user_uid = models.CharField(max_length=120) + user = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, blank=True, null=True + ) + client_id = models.URLField(blank=True, null=True) + sid = models.CharField( + max_length=1024, blank=True, null=True, + help_text=_("django session key") + ) + nonce = models.CharField(max_length=2048, blank=False, null=False) + authz_request = models.JSONField(max_length=65536, blank=False, null=False) + + revoked = models.BooleanField(default=False) + auth_code = models.CharField(max_length=2048, blank=False, null=False) + acr = models.CharField(max_length=1024, blank=False, null=False) + + def set_sid(self, request): + try: + Session.objects.get(session_key=request.session.session_key) + self.sid = request.session.session_key + except Exception: + logger.warning( + f"Error setting SID for OidcSession {self} " + "due to multiple authentication with different users with " + "the same browser." + ) + self.sid = self.auth_code + self.save() + + def revoke(self, destroy_session=True): + session = Session.objects.filter(session_key=self.sid) + if session and destroy_session: + session.delete() + self.revoked = True + iss_tokens = IssuedToken.objects.filter(session=self) + iss_tokens.update(revoked=True) + self.save() + + def pairwised_sub(self, provider_id:str): + return hashlib.sha256( + f"{self.user_uid}{self.client_id}{provider_id}{OIDCFED_PROVIDER_SALT}".encode() + ).hexdigest() + + def public_sub(self): + return hashlib.sha256( + f"{self.user_uid}{OIDCFED_PROVIDER_SALT}".encode() + ).hexdigest() + + def __str__(self): + return "{} {}".format(self.user_uid, self.auth_code) + + class Meta: + verbose_name = "User Session" + verbose_name_plural = "User Sessions" + unique_together = ("client_id", "nonce") + ordering = ["-created"] + + +class IssuedToken(TimeStampedModel): + session = models.ForeignKey(OidcSession, on_delete=models.CASCADE) + access_token = models.TextField(blank=True, null=True) + id_token = models.TextField(blank=True, null=True) + refresh_token = models.TextField(blank=True, null=True) + expires = models.DateTimeField() + revoked = models.BooleanField(default=False) + + class Meta: + verbose_name = "Issued Token" + verbose_name_plural = "Issued Tokens" + + @property + def client_id(self): + return self.session.client_id + + @property + def user_uid(self): + return self.session.user_uid + + @property + def expired(self): + return timezone.localtime() >= self.expires + + @property + def is_revoked(self): + return self.session.revoked or self.revoked + + def __str__(self): + return "{} @ {}".format(self.session.user_uid, self.session.client_id) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/processors.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/processors.py new file mode 100644 index 0000000..f1d8339 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/processors.py @@ -0,0 +1,8 @@ +import hashlib + + +def spidCode(user_info: dict, client_conf: dict, data: dict = None, kwargs: dict = {}): + + return hashlib.sha512( + f"{user_info['username']}{kwargs.get('salt', '')}".encode() + ).hexdigest() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_requests.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_requests.py new file mode 100644 index 0000000..f425c4e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_requests.py @@ -0,0 +1,222 @@ +from enum import Enum +from typing import List, Literal, Optional + +from pydantic import BaseModel, Field, HttpUrl, conlist, constr, validator + +# TODO: Schema of claims is not genrated + + +class AcrValues(str, Enum): + l1 = "https://www.spid.gov.it/SpidL1" + l2 = "https://www.spid.gov.it/SpidL2" + l3 = "https://www.spid.gov.it/SpidL3" + + +class ScopeSpid(str, Enum): + openid = "openid" + offline_access = "offline_access" + + +class ScopeCie(str, Enum): + openid = "openid" + offline_access = "offline_access" + profile = "profile" + email = "email" + + +class ClaimsTypeEssential(BaseModel): + essential: Optional[bool] + + +class ClaimsTypeStringValue(BaseModel): + value: Optional[str] + + +class ClaimsTypeStringValues(BaseModel): + values: Optional[conlist(str, max_items=2, min_items=2)] + + +class ClaimsType(str, Enum): + essential = ClaimsTypeEssential + value = ClaimsTypeStringValue + values = ClaimsTypeStringValues + + +class UserInfoSpid(BaseModel): + name: Optional[dict] = Field( + alias="given_name", default=None + ) + family_name: Optional[dict] = Field( + alias="family_name", default=None + ) + place_of_birth: Optional[dict] = Field( + alias="place_of_birth", default=None + ) + date_of_birth: Optional[dict] = Field( + alias="birthdate", default=None + ) + gender: Optional[dict] = Field( + alias="gender", default=None + ) + company_name: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/company_name", default=None + ) + registered_office: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/registered_office", default=None + ) + fiscal_number: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/fiscal_number", default=None + ) + iva_code: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/vat_number", default=None + ) + id_card: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/document_details", default=None + ) + mobile_phone: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/phone_number", default=None + ) + email: Optional[dict] = Field( + alias="email", default=None + ) + address: Optional[dict] = Field( + alias="address", default=None + ) + expiration_date: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/eid_exp_date", default=None + ) + digital_address: Optional[dict] = Field( + alias="https://attributes.eid.gov.it/e_delivery_service", default=None + ) + + +class UserInfoCie(BaseModel): + given_name: Optional[dict] + family_name: Optional[dict] + email: Optional[dict] + email_verified: Optional[dict] + gender: Optional[dict] + birthdate: Optional[dict] + phone_number: Optional[dict] + phone_number_verified: Optional[dict] + address: Optional[dict] + place_of_birth: Optional[dict] + document_details: Optional[dict] + e_delivery_service: Optional[dict] + fiscal_number: Optional[dict] + physical_phone_number: Optional[dict] + + +class IdToken(UserInfoCie): + pass + + +TYPES = { + "essential": ClaimsTypeEssential, + "value": ClaimsTypeStringValue, + "values": ClaimsTypeStringValues, +} + +CLAIMS_SPID = {"userinfo": UserInfoSpid} + +CLAIMS_CIE = {"userinfo": UserInfoCie, "id_token": IdToken} + + +class AuthenticationRequest(BaseModel): + client_id: HttpUrl + response_type: Literal["code"] + scope: List[str] + code_challenge: str + code_challenge_method: Literal["S256"] + nonce: constr(min_length=32) + redirect_uri: HttpUrl + claims: Optional[dict] + state: constr(min_length=32) + # TODO: to be improved + ui_locales: Optional[List[str]] + + # sub claim MUST not be used to prevent that this jwt + # could be reused as a private_key_jwt + # sub: HttpUrl + + iss: HttpUrl + iat: int + exp: Optional[int] + jti: Optional[str] + aud: str | List[HttpUrl] + acr_values: Optional[List[AcrValues]] + prompt: Optional[Literal["consent", "consent login"]] + + @validator("claims") + def validate_claims(cls, claims): + for k_claim, v_claim in claims.items(): + cl = cls.get_claims() + claims_items = cl.get(k_claim, None) + if not claims_items: + continue + claims_items(**v_claim) + for k_item, v_item in v_claim.items(): + if v_item is not None: + for k_type, v_type in TYPES.items(): + v_type(**v_item) + return claims + + @validator("scope") + def validate_scope(cls, scope): + if "openid" not in scope: + raise ValueError("'scope' attribute must contain 'openid'") + + +class AuthenticationRequestSpid(AuthenticationRequest): + scope: List[ScopeSpid] + + def get_claims() -> dict: + return CLAIMS_SPID + + def example(): # pragma: no cover + return AuthenticationRequestSpid( # nosec B106 + client_id= "https://rp.cie.it/callback1/", + response_type= "code", + scope= ["openid", "offline_access"], + code_challenge= "codeChallenge", + code_challenge_method= "S256", + nonce= "12345678123456781234567812345678inpiu", + prompt= "consent", + redirect_uri= "https://rp.cie.it/callback1/", + acr_values= ["https://www.spid.gov.it/SpidL2", "https://www.spid.gov.it/SpidL1"], + claims= {}, + state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd", + ui_locales= ["codice1", "codice2", "codice3"], + sub= "https://rp.cie.it/", + iss= "https://op.spid.agid.gov.it/", + aud= ["https://rp.spid.agid.gov.it/auth"], + iat= 1648591200, + exp= 1648592200, + jti= "a72d5df0-2415-4c7c-a44f-3988b354040b", + ) + + +class AuthenticationRequestCie(AuthenticationRequest): + scope: List[ScopeCie] + + def get_claims() -> dict: + return CLAIMS_CIE + + +class AuthenticationRequestDoc(BaseModel): + client_id: HttpUrl + response_type: Literal["code"] + scope: List[str] + code_challenge: str + code_challenge_method: Literal["S256"] + request: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + + def example(): # pragma: no cover + return AuthenticationRequestDoc( # nosec B106 + client_id= "https://rp.cie.it/callback1/", + response_type= "code", + scope= ["openid", "offline_access"], + code_challenge= "codeChallenge", + code_challenge_method= "S256", + request= "eyJhbGciOiJSUzI1NiIsImtpZCI6ImsyYmRjIn0.ew0KICJpc3MiOiAiczZCaGRSa3F0MyIsDQogImF1ZCI6ICJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsDQogInJlc3BvbnNlX3R5cGUiOiAiY29kZSBpZF90b2tlbiIsDQogImNsaWVudF9pZCI6ICJzNkJoZFJrcXQzIiwNCiAicmVkaXJlY3RfdXJpIjogImh0dHBzOi8vY2xpZW50LmV4YW1wbGUub3JnL2NiIiwNCiAic2NvcGUiOiAib3BlbmlkIiwNCiAic3RhdGUiOiAiYWYwaWZqc2xka2oiLA0KICJub25jZSI6ICJuLTBTNl9XekEyTWoiLA0KICJtYXhfYWdlIjogODY0MDAsDQogImNsYWltcyI6IA0KICB7DQogICAidXNlcmluZm8iOiANCiAgICB7DQogICAgICJnaXZlbl9uYW1lIjogeyJlc3NlbnRpYWwiOiB0cnVlfSwNCiAgICAgI.qq", + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_response.py new file mode 100644 index 0000000..1a9dc9d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/authn_response.py @@ -0,0 +1,44 @@ +from typing import Literal + +from pydantic import BaseModel, HttpUrl, constr + + +class AuthenticationResponse(BaseModel): + # TODO: is 1*VSCHAR + code: str + state: constr(min_length=32) + + def example(): # pragma: no cover + return AuthenticationResponse( # nosec B106 + code= "usDwMnEzJPpG5oaV8x3j&", + state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd", + ) + + +class AuthenticationResponseCie(AuthenticationResponse): + iss: HttpUrl + + def example(): # pragma: no cover + return AuthenticationResponse( # nosec B106 + code= "usDwMnEzJPpG5oaV8x3j&", + state= "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd", + iss= "https://idserver.servizicie.interno.gov.it", + ) + + +class AuthenticationErrorResponse(BaseModel): + error: Literal[ + "invalid_request", + "unauthorized_client", + "access_denied", + "unsupported_response_type", + "invalid_scope", + "server_error", + "temporarily_unavailable", + ] + error_description: str + state: constr(min_length=32) + + +class AuthenticationErrorResponseCie(AuthenticationErrorResponse): + iss: HttpUrl diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/client_assertion.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/client_assertion.py new file mode 100644 index 0000000..32d4935 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/client_assertion.py @@ -0,0 +1,32 @@ +from spid_cie_oidc.entity.utils import iat_now + +from pydantic import BaseModel, AnyHttpUrl, validator +from typing import Optional, List + + +class ClientAssertion(BaseModel): + iss: AnyHttpUrl + sub: AnyHttpUrl + iat: int + exp: int + jti: Optional[str] + aud: str | List[AnyHttpUrl] + + @validator("sub") + def iss_and_sub_must_match(cls, sub, values): + if values['iss'] != sub: + raise ValueError( + 'Client Assertion: iss and sub must have the same value' + ) + return sub + + @validator("exp") + def not_expired(cls, exp, values): + _now = iat_now() + if not (values['iat'] <= _now < exp): + raise ValueError( + 'Client Assertion: exp must be greater than ' + 'iat and less than the current time.' + f'{values["iat"]} <= {_now} < {exp}' + ) + return exp diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_request.py new file mode 100644 index 0000000..585ef9c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_request.py @@ -0,0 +1,11 @@ +from typing import Literal +from pydantic import BaseModel, HttpUrl, constr + + +class IntrospectionRequest(BaseModel): + client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + client_assertion_type: Literal[ + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ] + client_id: HttpUrl + token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_response.py new file mode 100644 index 0000000..6f3560d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/introspection_response.py @@ -0,0 +1,34 @@ +from typing import Literal + +from pydantic import BaseModel + + +class IntrospectionResponse(BaseModel): + active: bool + # scope: Optional[List[str]] + # sub: Optional[str] + # exp: Optional[int] + # client_id: HttpUrl + # iss: Optional[HttpUrl] + # TODO: migliorare: array di url con almeno uno + # aud: Optional[List[HttpUrl]] + + +class IntrospectionErrorResponse(BaseModel): + error_description: str + + +class IntrospectionErrorResponseSpid(IntrospectionErrorResponse): + error: Literal[ + "invalid_client", "invalid_request", "server_error", "temporarily_unavailable" + ] + + +class IntrospectionErrorResponseCie(IntrospectionErrorResponse): + error: Literal[ + "invalid_client", + "invalid_request", + "invalid_token", + "server_error", + "temporarily_unavailable", + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/jwt.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/jwt.py new file mode 100644 index 0000000..f50a89c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/jwt.py @@ -0,0 +1,67 @@ +import datetime +from typing import List + +from django.conf import settings +from django.utils import timezone +from pydantic import BaseModel, HttpUrl, validator +from spid_cie_oidc.entity.utils import datetime_from_timestamp + + +try: + import spid_cie_oidc.entity.settings as entity_settings + + NOW = timezone.localtime() + MAX_ACCEPTED_TIMEDIFF = getattr( + settings, "MAX_ACCEPTED_TIMEDIFF", entity_settings.MAX_ACCEPTED_TIMEDIFF + ) +except ImportError: # pragma: no cover + NOW = datetime.datetime.now() + MAX_ACCEPTED_TIMEDIFF = 5 + + +class JwtStructure(BaseModel): + iss: HttpUrl + sub: HttpUrl + iat: int + exp: int + jti: str + # TODO: migliorare: array di url con almeno uno + aud: List[HttpUrl] + + @validator("exp") + def validate_exp(cls, exp): + now = int(NOW.timestamp()) + if exp <= now: + raise ValueError("exp MUST be in the future") + + @validator("iat") + def validate_iat(cls, iat): + dt_iat = datetime_from_timestamp(iat) + delta = datetime.timedelta(minutes=MAX_ACCEPTED_TIMEDIFF) + if abs((NOW - dt_iat).total_seconds()) > delta.total_seconds(): + raise ValueError(f"iat MUST be in the last {MAX_ACCEPTED_TIMEDIFF} minutes") + + +class IdTokenJwt(JwtStructure): + # TODO: implementare + pass + + +class AccessTokenJwt(JwtStructure): + # TODO: implementare + pass + + +class RefreshTokenJwt(JwtStructure): + # TODO: implementare + pass + + +class UserInfoResponseJwtHeader(BaseModel): + # TODO: implementare + pass + + +class UserInfoResponseJwtPayload(JwtStructure): + # TODO: implementare + pass diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_request.py new file mode 100644 index 0000000..c1262e7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_request.py @@ -0,0 +1,12 @@ +from typing import Literal + +from pydantic import BaseModel, HttpUrl, constr + + +class RevocationRequest(BaseModel): + client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + client_assertion_type: Literal[ + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ] + client_id: HttpUrl + token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_response.py new file mode 100644 index 0000000..df8f168 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/revocation_response.py @@ -0,0 +1,14 @@ +from typing import Literal + +from pydantic import BaseModel + + +class RevocationErrorResponse(BaseModel): + error: Literal[ + "invalid_request", + "invalid_client", + "unauthorized_client", + "invalid_scope", + "unsupported_token_type", + ] + error_description: str diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_requests.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_requests.py new file mode 100644 index 0000000..33e1c47 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_requests.py @@ -0,0 +1,43 @@ +from typing import Literal + +from pydantic import BaseModel, HttpUrl, constr + + +class TokenRequest(BaseModel): + client_id: HttpUrl + client_assertion: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + client_assertion_type: Literal[ + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ] + + +class TokenAuthnCodeRequest(TokenRequest): + # TODO: is 1*VSCHAR + code: str + # TODO: is 43*128unreserved, where unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + code_verifier: str + grant_type: Literal["authorization_code"] + + def example(): + return TokenAuthnCodeRequest( + client_id = "http://example.com", + client_assertion = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ", + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + code = "bb03ad1f2e838335c1050ae280a64f4e195dbeb17ba8aa2dc6fd99e1c5181fc735be6af10b634fbf0f04f533843e199d0261bf570585ca292cb4fb8857480af5", + code_verifier = "nd6gJEXzUZApV46u4DmVUfZgBYc2SJsZGkaBftof4gcI95ByALg", + grant_type = "authorization_code" + ) + + +class TokenRefreshRequest(TokenRequest): + grant_type: Literal["refresh_token"] + refresh_token: str + + def example(): + return TokenRefreshRequest( # nosec B106 + client_id = "http://example.com", + client_assertion = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ", + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRCNjdnTDdjazNURmlJQWY3TjZfN1NIdnFrME1EWU1FUWNvR0dsa1VBQXcifQ.eyJpc3MiOiJodHRwOi8vb3AtdGVzdC9vaWRjL29wLyIsInN1YiI6Imh0dHA6Ly9ycC10ZXN0Lml0L29pZGMvcnAvIiwiZXhwIjoxNjQ4NDk3OTU2LCJpYXQiOjE2NDg0OTU5NzYsImF1ZCI6WyJodHRwOi8vcnAtdGVzdC5pdC9vaWRjL3JwLyJdLCJjbGllbnRfaWQiOiJodHRwOi8vcnAtdGVzdC5pdC9vaWRjL3JwLyIsInNjb3BlIjoib3BlbmlkIn0.yDF6BsFpT1h2JdSxyE3hE8chcF0Lq0OtGzRoZkBbN7otHTvAsTcupT-J6JTHnO817Bp1kllvLkJ5X4p3NeElOL-TbzF1xtX2azpiaxZD4xEGOYsMGkRnLeJP9LWGYbJXEBlAjHMpYW-DhZz3cE3iJvmaehi6sIRsXMPszsDIrt2cYmwFg9KrEc2Ty_qWKdIhzgxq82FEgMk2e2ehez3MTH7KAni2hRmtl32w6uf7_cfW5GlLfDjjVNm2Y9NkmrFeKmX0YUOQWXTts1MhlGfnw9ASgBstgfB5tWWmxF6Sg7ojAnPt_eKH75gcMLhUxj4iif8Aj0QNPzXuJ7VlBH9VAA", + grant_type = "refresh_token" + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_response.py new file mode 100644 index 0000000..1cf2589 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/schemas/token_response.py @@ -0,0 +1,19 @@ +from typing import Literal + +from pydantic import BaseModel, constr + + +class TokenResponse(BaseModel): + access_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + token_type: Literal["Bearer"] + expires_in: int + id_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + + +class TokenRefreshResponse(TokenResponse): + refresh_token: constr(regex=r"^[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+\.[a-zA-Z\_\-0-9]+") # noqa: F722 + + +class TokenErrorResponse(BaseModel): + error: Literal["invalid_request", "unauthorized_client"] + error_description: str diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/settings.py new file mode 100644 index 0000000..41b45ee --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/settings.py @@ -0,0 +1,207 @@ +from copy import deepcopy +from django.conf import settings +from django.utils.translation import gettext as _ +from spid_cie_oidc.entity.schemas.op_metadata import ( + OPMetadataCie, + OPMetadataSpid +) +from spid_cie_oidc.provider.schemas.authn_requests import ( + AcrValues, + AuthenticationRequestCie, + AuthenticationRequestDoc, + AuthenticationRequestSpid +) +from spid_cie_oidc.provider.schemas.authn_response import AuthenticationErrorResponse, AuthenticationErrorResponseCie, \ + AuthenticationResponse, AuthenticationResponseCie +from spid_cie_oidc.provider.schemas.introspection_request import IntrospectionRequest +from spid_cie_oidc.provider.schemas.introspection_response import IntrospectionErrorResponseCie, \ + IntrospectionErrorResponseSpid, IntrospectionResponse +from spid_cie_oidc.provider.schemas.revocation_request import RevocationRequest +from spid_cie_oidc.provider.schemas.revocation_response import RevocationErrorResponse +from spid_cie_oidc.provider.schemas.token_requests import TokenAuthnCodeRequest, TokenRefreshRequest +from spid_cie_oidc.provider.schemas.token_response import TokenErrorResponse, TokenRefreshResponse, TokenResponse + + +OIDCFED_PROVIDER_PROFILES_MEDIA = getattr( + settings, + "OIDCFED_PROVIDER_PROFILES_MEDIA", + { + "spid": { + "logo": "svg/spid-logo-c-lb.svg", + "arc_position": "" + }, + "cie": { + "logo": "svg/logo-cie.svg", + "arc_position": "mt-5" + }, + }, +) +OIDCFED_PROVIDER_ATTRIBUTES_SPID_MAP = { + "https://attributes.eid.gov.it/spid_code": ( + { + "func": "spid_cie_oidc.provider.processors.spidCode", + "kwargs": {"salt": "signiicusenz"}, + }, + ), + "given_name": ("given_name",), + "family_name": ("family_name",), + "place_of_birth": ("place_of_birth",), + "birthdate": ("birthdate",), + "gender": ("gender",), + "https://attributes.eid.gov.it/company_name": ("company_name",), + "https://attributes.eid.gov.it/registered_office": ("registered_office",), + "https://attributes.eid.gov.it/fiscal_number": ("fiscal_number", ), + "https://attributes.eid.gov.it/company_fiscal_number": ("company_fiscal_number",), + "https://attributes.eid.gov.it/vat_number": ("vat_number",), + "document_details": ("document_details",), + "phone_number": ( + "phone_number", + "mobile_phone", + "phone", + "telephone", + ), + "email": ("email",), + "https://attributes.eid.gov.it/e_delivery_service": ("e_delivery_service",), + "https://attributes.eid.gov.it/eid_exp_date": ("eid_exp_date",), + "address": ("address",), +} + +OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP = { + "given_name": ("name", "given_name"), + "family_name": ("family_name", "surname"), + "email": ("email",), + "fiscal_number": ("fiscal_number", "tin"), + "email_verified": ("email",), + "gender": ("gender",), + "birthdate": ("date_of_birth",), + "phone_number": ("mobile_phone", "phone", "telephone"), + "phone_number_verified": ("mobile_phone", "phone", "telephone"), + "address": ("address",), + "place_of_birth": ("place_of_birth",), + # "document_details": , + # "e_delivery_service": , + "physical_phone_number": ("mobile_phone", "phone", "telephone"), +} + +OIDCFED_PROVIDER_ATTRIBUTES_MAP = deepcopy(OIDCFED_PROVIDER_ATTRIBUTES_SPID_MAP) +OIDCFED_PROVIDER_ATTRIBUTES_MAP.update(OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP) + +OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS = dict( + spid = dict(), + cie = OIDCFED_PROVIDER_ATTRIBUTES_CIE_MAP +) + +OIDCFED_PROVIDER_PROFILES = getattr( + settings, + "OIDCFED_PROVIDER_PROFILES", + { + "spid": { + "authorization_request": AuthenticationRequestSpid, + "authorization_request_doc": AuthenticationRequestDoc, + "authorization_response": AuthenticationResponse, + "authorization_error_response": AuthenticationErrorResponse, + "op_metadata": OPMetadataSpid, + "authorization_code": TokenAuthnCodeRequest, + "authorization_code_response": TokenResponse, + "refresh_token": TokenRefreshRequest, + "refresh_token_response": TokenRefreshResponse, + "token_error_response": TokenErrorResponse, + "revocation_request": RevocationRequest, + "revocation_response": RevocationErrorResponse, + "introspection_request" : IntrospectionRequest, + "introspection_response" : IntrospectionResponse, + "introspection_error_response" : IntrospectionErrorResponseSpid, + }, + "cie": { + "authorization_request": AuthenticationRequestCie, + "authorization_request_doc": AuthenticationRequestDoc, + "authorization_response": AuthenticationResponseCie, + "authorization_error_response": AuthenticationErrorResponseCie, + "op_metadata": OPMetadataCie, + "authorization_code": TokenAuthnCodeRequest, + "authorization_code_response": TokenResponse, + "refresh_token": TokenRefreshRequest, + "refresh_token_response": TokenRefreshResponse, + "token_error_response": TokenErrorResponse, + "revocation_request": RevocationRequest, + "revocation_response": RevocationErrorResponse, + "introspection_request" : IntrospectionRequest, + "introspection_response" : IntrospectionResponse, + "introspection_error_response" : IntrospectionErrorResponseCie, + }, + }, +) + +OIDCFED_PROVIDER_SALT = getattr(settings, "OIDCFED_PROVIDER_SALT", "CHANGEME") +OIDCFED_DEFAULT_PROVIDER_PROFILE = getattr(settings, "OIDCFED_PROVIDER_PROFILE", "spid") + +OIDCFED_PROVIDER_MAX_REFRESH = getattr(settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1) +OIDCFED_PROVIDER_HISTORY_PER_PAGE = getattr(settings, "OIDCFED_PROVIDER_HISTORY_PER_PAGE", 50) + +# lifetime of validity of an auth code +OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE = getattr( + settings, + "OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE", + 10 +) + +OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME = getattr( + settings, + "OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME", + 2 +) + +OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR = getattr( + settings, + "OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR", + dict( + spid = AcrValues.l2.value, + cie = AcrValues.l2.value + ) +) + +OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH = getattr( + settings, + "OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH", + dict( + spid = [AcrValues.l1.value], + cie = [AcrValues.l1.value] + ) +) + + +# SPID and CIE attribute labels (CIE entries override SPID for shared keys) +_OIDCFED_ATTRNAME_I18N_SPID = { + "given_name": _("Name"), + "family_name": _("Family name"), + "place_of_birth": _("Place of birth"), + "birthdate": _("Date of birth"), + "gender": _("Gender"), + "https://attributes.eid.gov.it/company_name": _("Company Name"), + "https://attributes.eid.gov.it/registered_office": _("Registered Office"), + "https://attributes.eid.gov.it/fiscal_number": _("Tax payer id"), + "https://attributes.eid.gov.it/vat_number": _("Vat number"), + "document_details": _("Id card"), + "phone_number": _("Mobile phone"), + "email": _("Email"), + "address": _("Address"), + "https://attributes.eid.gov.it/eid_exp_date": _("Expiration date"), + "https://attributes.eid.gov.it/e_delivery_service": _("Digital address"), +} +_OIDCFED_ATTRNAME_I18N_CIE = { + "given_name": _("Name"), + "family_name": _("Family name"), + "email": _("Email"), + "fiscal_number": _("Tax payer id"), + "email_verified": _("Email verified"), + "gender": _("Gender"), + "birthdate": _("Date of birth"), + "phone_number": _("Phone number"), + "phone_number_verified": _("Verified phone"), + "address": _("Address"), + "place_of_birth": _("Place of birth"), + # "document_details": , + # "e_delivery_service": , + "physical_phone_number": _("Phone number"), +} +OIDCFED_ATTRNAME_I18N = {**_OIDCFED_ATTRNAME_I18N_SPID, **_OIDCFED_ATTRNAME_I18N_CIE} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/css/provider.css b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/css/provider.css new file mode 100644 index 0000000..3b41ca5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/css/provider.css @@ -0,0 +1,352 @@ +@import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600); +.italia-it-button { + display: inline-block; + position: relative; + padding: 0; + color: #FFF; + font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-weight: 600; + line-height: 1em; + text-decoration: none; + border: 0; + text-align: center; + cursor: pointer; + border-radius: 15px; +} + +.italia-it-button-icon, +.italia-it-button-text { + display: block; + float: left +} + +.italia-it-button-icon { + margin: 0 -.4em 0 0; + padding: 0.6em .8em .5em; + border-right: rgba(255, 255, 255, 0.1) 0.1em solid +} + +.italia-it-button-text { + padding: .95em 1em .85em 1em; + font-size: 1.15em; + text-align: center +} + +svg { + width: 1.8em; + height: 1.8em; + fill: #fff +} + +.italia-it-block { + display: block +} + +.italia-it-button-size-s { + font-size: 10px; + width: 150px; + border-radius: 5px; +} + +.italia-it-button-size-s > span img { + width: 19px; + height: 19px; + border: 0 +} + +.italia-it-button-size-m { + font-size: 15px; + width: 220px; + border-radius: 0px; +} + +.italia-it-button-size-m > span img { + width: 29px; + height: 29px; + border: 0 +} + +.italia-it-button-size-l { + font-size: 20px; + width: 280px +} + +.italia-it-button-size-l > span img { + width: 38px; + height: 38px; + border: 0 +} + +.italia-it-button-size-xl { + font-size: 24px; + width: 340px; +} + +.italia-it-button-size-xl > span img { + width: 47px; + height: 47px; + border: 0 +} + +.button-spid { + background-color: #06C; + color: #FFF +} + +.button-spid svg { + fill: #FFF +} + +.button-spid:hover { + background-color: #036; + color: #FFF +} + +.button-spid:active { + background-color: #83BEED; + color: #036 +} + +.button-cie { + background-color: #4c5c6c; + color: #FFF +} + +.button-cie svg { + fill: #FFF +} + +.button-cie:hover { + background-color: #414c58; + color: #FFF +} + +.button-cie:active { + background-color: #394046; + color: #036 +} + +.spid-idp-button { + position: absolute; + z-index: 1039; + display: none +} + +.spid-idp-button .spid-idp-button-menu, +.spid-idp-button .spid-idp-button-panel { + list-style: none; + background: white; + border: solid 1px #ddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + overflow: visible; + padding: 0; + margin: 0 +} + +#spid-idp-button-small-get, +#spid-idp-button-medium-get, +#spid-idp-button-small-post, +#spid-idp-button-medium-post { + width: 230px +} + +#spid-idp-button-large-get, +#spid-idp-button-large-post { + width: 270px +} + +#spid-idp-button-xlarge-get, +#spid-idp-button-xlarge-post { + width: 330px +} + +.spid-idp-button .spid-idp-button-panel { + padding: 10px +} + +.spid-idp-button.spid-idp-button-tip { + margin-top: 8px +} + +.spid-idp-button.spid-idp-button-tip:before { + position: absolute; + top: -6px; + left: 9px; + content: ""; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ddd; + display: inline-block +} + +.spid-idp-button.spid-idp-button-tip:after { + position: absolute; + top: -5px; + left: 10px; + content: ""; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + display: inline-block +} + +.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:before { + left: auto; + right: 9px +} + +.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:after { + left: auto; + right: 10px +} + +.spid-idp-button.spid-idp-button-scroll .spid-idp-button-menu, +.spid-idp-button.spid-idp-button-scroll .spid-idp-button-panel { + max-height: 180px; + overflow: auto +} + +.spid-idp-button .spid-idp-button-menu li { + list-style: none; + padding: 0 0; + margin: 0; + line-height: 18px +} + +.spid-idp-button .spid-idp-button-menu li > a, +.spid-idp-button .spid-idp-button-menu label { + display: block; + font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-weight: 600; + font-size: .9em; + color: #3b61b6; + text-decoration: underline; + line-height: 18px; + padding-top: 5px; + white-space: nowrap; + border-bottom: 1px solid #ddd; +} + +.spid-idp-button .spid-idp-button-menu li > a:hover, +.spid-idp-button .spid-idp-button-menu label:hover { + color: #036; + cursor: pointer; + background-color: #F0F0F0 +} + +.spid-idp-button .spid-idp-button-menu li > a img { + height: 35px; + padding-bottom: 5px; + border: 0; + width: 100%; +} + +.idp-button-idp-logo { + font-size: 100%; + height: 10%; + width: 100%; + border: 0; + border-bottom: 1px solid #CCC; + background-color: #FFF; + padding: 15px; + text-align: left; + cursor: pointer +} + +.idp-button-idp-logo:hover { + background-color: #F0F0F0 +} + +.idp-button-idp-logo img { + height: 25px; + vertical-align: middle; + cursor: pointer; +} + +.spid-top-menu-space-10 > a img { + margin-top: 10px +} + +.spid-top-menu-space-20 > a img { + margin-top: 20px +} + +.spid-top-menu-line { + border-top: 5px solid #000 +} + +.spid-idp-support-link > a { + padding: 5px 0 10px 10px +} + +.spid-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0 +} + + +.button-login { + border-radius: 30px !important; + background-color: #06C; + color: #FFF +} + +.button-login svg { + fill: #FFF +} + +.button-login:hover { + background-color: #036; + color: #FFF +} + +.button-login:active { + background-color: #83BEED; + color: #036 +} + +.italia-it-button-cancel-m { + font-size: 15px; + width: 220px; + border-radius: 0px; +} + +.italia-it-button-cancel-s { + font-size: 15px; + width: 150px; + border-radius: 0px; +} + +.button-cancel { + border-radius: 30px !important; + border: 1px solid #0066cc !important; + background-color: #FFF; + color: #3d4955; +} + +.button-cancel:hover { + background-color: #0066cc; + color: #FFF +} + +.button-cancel:active { + background-color: #83BEED; + color: #036 +} + +.italia-it-button-icon, +.italia-it-buttons-text { + display: block; +} + +.italia-it-buttons-text { + padding: .95em 1em .85em 1em; + font-size: 1.15em; + text-align: center +} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/images/logo-cie.png b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/images/logo-cie.png new file mode 100644 index 0000000000000000000000000000000000000000..a6bfd6ada3a6a2c2b0e3c318c70b204ef0b01493 GIT binary patch literal 196297 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�yyjK_RTj$v3_{3sIt-V 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

0&UTwaXA?8d{izIsmh>(m6ki_QRO} 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

+0rI2NG;l~&( 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 tylZolLeJ&rS64!D#Cx7k??3ltOB>l-y(cyJL \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/logo-cie.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/logo-cie.svg new file mode 100644 index 0000000..6a86246 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/logo-cie.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/no_image_available.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/no_image_available.svg new file mode 100644 index 0000000..7b06f8f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/no_image_available.svg @@ -0,0 +1,14 @@ + + + + + + NO IMAGEAVAILABLE + + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-ico-circle-bb.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-ico-circle-bb.svg new file mode 100644 index 0000000..c15a713 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-ico-circle-bb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg new file mode 100644 index 0000000..c8d1dbd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg-LTS.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg-LTS.svg new file mode 100644 index 0000000..70d2caa --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/spid-logo-c-lb.svg-LTS.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + LONG TERMSESSION + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_base.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_base.html new file mode 100644 index 0000000..21a5c3b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_base.html @@ -0,0 +1,91 @@ + +{% extends 'bootstrap-italia-base.html' %} + +{% load static %} +{% load i18n %} + +{% block extra_head %} + {{ block.super }} + +{% endblock extra_head %} + +{% block page_title %} +{% trans "OIDC Provider" %} +{% endblock page_title %} + + +{% block header_slim_org_url %} +https://www.spid.gov.it/ +{% endblock header_slim_org_url %} + + +{% block header_slim_org_name %} + {% trans "OIDC Provider" %} +{% endblock header_slim_org_name %} + + +{% block header_slim_mobile_org_name %} + {% trans "OIDC Provider" %} +{% endblock header_slim_mobile_org_name %} + + +{% block header_mobile_arrow %}{% endblock header_mobile_arrow %} +{% block header_mobile_slim_menu %}{% endblock header_mobile_slim_menu %} + + +{% block header_center_logo %} +{% endblock header_center_logo %} + + + +{% block header_center_org_name %} +{% trans "OIDC Provider" %} +{% endblock header_center_org_name %} + + + +{% block header_center_org_subname %} +{% endblock header_center_org_subname %} + +{% block container %} +

+
+
+
+
+
+
+
+ {% block centered_container %}{% endblock %} +
+
+
+
+
+
+
+
+{% endblock container %} + + +{% block footer_logo %} +{% endblock footer_logo %} + + +{% block footer_org_name %} + {% trans "OIDC Provider" %} +{% endblock footer_org_name %} + + +{% block footer_org_subname %} + {% trans "OIDC Provider" %} +{% endblock footer_org_subname %} + +{% block extra_scripts %} +{% block bootstrap_italia_validation %} + +{% endblock bootstrap_italia_validation %} +{% endblock extra_scripts %} + +{% block footer_menu_section %} +{% endblock footer_menu_section %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_consent.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_consent.html new file mode 100644 index 0000000..3847395 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_consent.html @@ -0,0 +1,109 @@ +{% extends 'op_base.html' %} +{% load i18n %} +{% load static %} +{% load spid_cie_op %} + +{% block menu_links %} + + + + +{% endblock menu_links %} + +{% block centered_container %} + +
+ +
+
+
+ + Home + +
+
+
+
+ +
+
+
+

{% trans 'SPID level access request from' %}

+

{{ client_organization_name }}

+
+
+
+ +
+
+

+ {% trans 'For the proper functioning of the requestor service you must give your consent to the release of the following data '%} +

+
+
+ + {% csrf_token %} +
+
+ + {% for form_field in form.visible_fields %} +
+
+ + {% if form_field.field.widget.input_type%} + + {% else%} + + {% endif %} +
+ {{ form_field.errors }} +
+ {% endfor %} + + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + +
+
+
    + {% for claim in user_claims %} +
  • {{ claim }}
  • + {% endfor %} +
+
+ + {% trans "DO NOT AGREE" %} + + +
+
+
+ +
+
+ +
+ + + +{% endblock %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_history.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_history.html new file mode 100644 index 0000000..46feb8d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_history.html @@ -0,0 +1,137 @@ +{% extends 'op_base.html' %} +{% load i18n %} +{% load static %} +{% load spid_cie_op %} + +{% block menu_links %} + + + + +{% endblock menu_links %} + +{% block centered_container %} + +
+ +
+
+
+ + Home + +
+
+
+
+ +
+

+ {% blocktrans %} + Access history of {{user}} + {% endblocktrans%} +

+
+ +
+
+

+ {% blocktrans%} + Accesses logs and status are listed below, + you can revoke an access to a Relying Party by clicking on "revoke" button + {% endblocktrans %} +

+
+
+ + {% csrf_token %} + + + + + + + + + + + + + {% for session in history %} + + + + + + {% if session.revoked %} + + {% else %} + + {% endif %} + + {% endfor %} + +
#{% trans "Relying party" %}{% trans "Sid" %}{% trans "Created" %}{% trans "Revoked" %}
{{ forloop.counter }}{{ session.client_id }}{{ session.sid|truncatechars:33 }}{{ session.created | date:'Y-m-d H:i'}} + {{ session.revoked }} + + {% trans "Revoke" %} +
+ + +
+ +{% endblock %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_login.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_login.html new file mode 100644 index 0000000..8a6549d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templates/op_user_login.html @@ -0,0 +1,118 @@ +{% extends 'op_base.html' %} +{% load i18n %} +{% load static %} +{% load spid_cie_op %} + +{% block menu_links %}{% endblock menu_links %} +{% block centered_container %} + +
+
+
+
+
+ + Home + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+

{% trans 'Authentication request from' %} +

+

{{ client_organization_name }}

+
+
+
+
+
+
+
+
+
+
+
+ {% csrf_token %} + {% for form_field in form.visible_fields %} +
+
+ + {% if form_field.field.widget.input_type%} + + {% endif %} +
+ {{ form_field.errors }} +
+ {% endfor %} + + {% for hidden in hidden_form.hidden_fields %} + {{ hidden }} + {% endfor %} + +
+ + {% trans "Cancel" %} + + + +
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + + +{% endblock %} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/spid_cie_op.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/spid_cie_op.py new file mode 100644 index 0000000..13626f6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/templatetags/spid_cie_op.py @@ -0,0 +1,24 @@ +from django import template +from django.conf import settings + + +from spid_cie_oidc.provider.settings import ( + OIDCFED_PROVIDER_PROFILES_MEDIA, + OIDCFED_DEFAULT_PROVIDER_PROFILE +) + +register = template.Library() + + +@register.simple_tag +def oidc_provider_logo(): + return f"""{settings.STATIC_URL}{OIDCFED_PROVIDER_PROFILES_MEDIA.get( + OIDCFED_DEFAULT_PROVIDER_PROFILE, {} + ).get('logo', "")}""" + + +@register.simple_tag +def oidc_provider_arc_value_position(): + return f"""{OIDCFED_PROVIDER_PROFILES_MEDIA.get( + OIDCFED_DEFAULT_PROVIDER_PROFILE, {} + ).get('arc_position', "")}""" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_request_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_request_settings.py new file mode 100644 index 0000000..a22e1a2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_request_settings.py @@ -0,0 +1,138 @@ +from copy import deepcopy + +from spid_cie_oidc.entity.utils import exp_from_now, iat_now + +# TODO: we need factory function to get fresh now +IAT = iat_now() +EXP = exp_from_now() + +CLAIMS_SPID = { + "userinfo": { + "given_name": {"values": ["str", "str"]}, + "family_name": None, + "birthdate": {"value": "str"}, + }, +} + +CLAIMS_CIE = { + "userinfo": { + "given_name": {"values": ["str", "str"]}, + "family_name": None, + "birthdate": {"value": "str"}, + }, + "id_token": { + "given_name": {"values": ["str", "str"]}, + "family_name": None, + "birthdate": {"value": "str"}, + }, +} + +AUTHN_REQUEST_SPID = { + "client_id": "https://rp.cie.it/callback1/", + "response_type": "code", + "scope": ["openid", "offline_access"], + "code_challenge": "codeChallenge", + "code_challenge_method": "S256", + "nonce": "12345678123456781234567812345678inpiu", + "prompt": "consent", + "redirect_uri": "https://rp.cie.it/callback1/", + "acr_values": ["https://www.spid.gov.it/SpidL2", "https://www.spid.gov.it/SpidL1"], + "claims": CLAIMS_SPID, + "state": "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd", + "ui_locales": ["codice1", "codice2", "codice3"], + "sub": "https://rp.cie.it/", + "iss": "https://op.spid.agid.gov.it/", + "aud": ["https://rp.spid.agid.gov.it/auth"], + "iat": IAT, + "exp": EXP, + "jti": "a72d5df0-2415-4c7c-a44f-3988b354040b", +} + +AUTHN_REQUEST_SPID_NO_CLIENT_ID = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CLIENT_ID.pop("client_id") + +AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS["claims"]["userinfo"][ + "given_name" +]["values"] = ["str"] + +AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE.pop("code_challenge") + +AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD.pop("code_challenge_method") + +AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD["code_challenge_method"] = "S257" + +AUTHN_REQUEST_SPID_NO_NONCE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_NONCE.pop("nonce") + +AUTHN_REQUEST_SPID_NO_CORRECT_NONCE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_NONCE["nonce"] = "toosmall" + +AUTHN_REQUEST_SPID_NO_PROMPT = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_PROMPT.pop("prompt") + +AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT["prompt"] = "no_correct" + +AUTHN_REQUEST_SPID_NO_REDIRECT_URL = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_REDIRECT_URL.pop("redirect_uri") + +AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL["redirect_uri"] = "https://rp" + +AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE.pop("response_type") + +AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE["response_type"] = "codecode" + +AUTHN_REQUEST_SPID_NO_SCOPE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_SCOPE.pop("scope") + +AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE["scope"] = ["offline_access"] + +AUTHN_REQUEST_SPID_NO_ACR_VALUES = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_ACR_VALUES.pop("acr_values") + +AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES["acr_values"] = [ + "https://www.spid.gov.it/SpidL4", + "https://www.spid.gov.it/SpidL1", +] + +AUTHN_REQUEST_SPID_NO_STATE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_STATE.pop("state") + +AUTHN_REQUEST_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_STATE["state"] = "toosmall" + +AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES["ui_locales"] = "nocorrect" + + +AUTHN_REQUEST_CIE = deepcopy(AUTHN_REQUEST_SPID) +AUTHN_REQUEST_CIE["scope"] = ["openid", "offline_access", "email", "profile"] +AUTHN_REQUEST_CIE["prompt"] = "consent login" +AUTHN_REQUEST_CIE["claims"] = CLAIMS_CIE + +AUTHN_REQUEST_CIE_NO_ACR_VALUES = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_ACR_VALUES.pop("acr_values") + +AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES["acr_values"] = "CIE_L3" + +AUTHN_REQUEST_CIE_NO_PROMPT = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_PROMPT.pop("prompt") + +AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT["prompt"] = "verify" + +AUTHN_REQUEST_CIE_NO_SCOPE = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_SCOPE.pop("scope") + +AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE = deepcopy(AUTHN_REQUEST_CIE) +AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE["scope"] = "openid" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_responses_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_responses_settings.py new file mode 100644 index 0000000..1820cbd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/authn_responses_settings.py @@ -0,0 +1,52 @@ +from copy import deepcopy + +AUTHN_RESPONSE_SPID = { + "code": "usDwMnEzJPpG5oaV8x3j&", + "state": "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd&", +} + +AUTHN_RESPONSE_SPID_NO_CODE = deepcopy(AUTHN_RESPONSE_SPID) +AUTHN_RESPONSE_SPID_NO_CODE.pop("code") + +AUTHN_RESPONSE_SPID_NO_STATE = deepcopy(AUTHN_RESPONSE_SPID) +AUTHN_RESPONSE_SPID_NO_STATE.pop("state") + +AUTHN_RESPONSE_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_RESPONSE_SPID) +AUTHN_RESPONSE_SPID_NO_CORRECT_STATE["state"] = "tooshort" + +AUTHN_RESPONSE_CIE = deepcopy(AUTHN_RESPONSE_SPID) +AUTHN_RESPONSE_CIE["iss"] = "https://idserver.servizicie.interno.gov.it" + +AUTHN_RESPONSE_CIE_NO_ISS = deepcopy(AUTHN_RESPONSE_CIE) +AUTHN_RESPONSE_CIE_NO_ISS.pop("iss") + +AUTHN_RESPONSE_CIE_NO_CORRECT_ISS = deepcopy(AUTHN_RESPONSE_CIE) +AUTHN_RESPONSE_CIE_NO_CORRECT_ISS["iss"] = "" + +AUTHN_ERROR_RESPONSE = { + "error_description": "request%20malformata&", + "code": "usDwMnEzJPpG5oaV8x3j&", + "state": "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd&", +} + +AUTHN_ERROR_RESPONSE_SPID = deepcopy(AUTHN_ERROR_RESPONSE) +AUTHN_ERROR_RESPONSE_SPID["error"] = "invalid_request" + +AUTHN_ERROR_RESPONSE_SPID_NO_CODE = deepcopy(AUTHN_ERROR_RESPONSE) +AUTHN_ERROR_RESPONSE_SPID_NO_CODE.pop("code") + +AUTHN_ERROR_RESPONSE_SPID_NO_STATE = deepcopy(AUTHN_ERROR_RESPONSE) +AUTHN_ERROR_RESPONSE_SPID_NO_STATE.pop("state") + +AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE = deepcopy(AUTHN_ERROR_RESPONSE) +AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE["state"] = "tooshort" + +AUTHN_ERROR_RESPONSE_CIE = deepcopy(AUTHN_ERROR_RESPONSE) +AUTHN_ERROR_RESPONSE_CIE["error"] = "unsupported_response_type" +AUTHN_ERROR_RESPONSE_CIE["iss"] = "https://idserver.servizicie.interno.gov.it" + +AUTHN_ERROR_RESPONSE_CIE_NO_ISS = deepcopy(AUTHN_ERROR_RESPONSE_CIE) +AUTHN_ERROR_RESPONSE_CIE_NO_ISS.pop("iss") + +AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS = deepcopy(AUTHN_ERROR_RESPONSE_CIE) +AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS["iss"] = "//idserver.servizicie.interno.gov.it" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_request_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_request_settings.py new file mode 100644 index 0000000..ef8b4e5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_request_settings.py @@ -0,0 +1,32 @@ +from copy import deepcopy + +INTROSPECTION_REQUEST = { + "client_assertion": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI.LVyRDPVJ", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_id": "https://rp.spid.agid.gov.it", + "token": "eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE0MTg3MDI0MTQsImF1ZCI6W.FXDtEzDL", +} + +INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion") + +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = ".payload.sig" + +INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type") + +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE["client_assertion_type"] = "" + +INTROSPECTION_REQUEST_NO_CLIENT_ID = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CLIENT_ID.pop("client_id") + +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID["client_id"] = "https//rp.spid.agid.gov.it" + +INTROSPECTION_REQUEST_NO_TOKEN = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_TOKEN.pop("token") + +INTROSPECTION_REQUEST_NO_CORRECT_TOKEN = deepcopy(INTROSPECTION_REQUEST) +INTROSPECTION_REQUEST_NO_CORRECT_TOKEN["token"] = ".sign" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_response_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_response_settings.py new file mode 100644 index 0000000..6d80be4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/introspection_response_settings.py @@ -0,0 +1,69 @@ +from copy import deepcopy + + +INTROSPECTION_RESPONSE = { + "active": True, + # "scope": ["openid"], + # "exp": 162545274, + # "sub": "OP-1234567890", + # "client_id": "https://rp.agid.gov.it/", + # "iss": "https://op.spid.agid.gov.it/", + # "aud": ["https://rp.spid.agid.gov.it/auth"], +} + + +INTROSPECTION_RESPONSE_NO_ACTIVE = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_ACTIVE.pop("active") + +INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE["active"] = "ciao" + +INTROSPECTION_RESPONSE_NO_CORRECT_SCOPE = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_SCOPE["scope"] = "scope" + +INTROSPECTION_RESPONSE_NO_CORRECT_EXP = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_EXP["exp"] = "exp" + +INTROSPECTION_RESPONSE_NO_CORRECT_SUB = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_SUB["sub"] = [] + +INTROSPECTION_RESPONSE_NO_CORRECT_CLIENT_ID = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_CLIENT_ID["client_id"] = "https/op.agid.gov.it/" + +INTROSPECTION_RESPONSE_NO_CORRECT_ISS = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_ISS["iss"] = "" + +INTROSPECTION_RESPONSE_NO_CORRECT_AUD = deepcopy(INTROSPECTION_RESPONSE) +INTROSPECTION_RESPONSE_NO_CORRECT_AUD["aud"] = "https://rp.spid.agid.gov.it/auth" + + +INTROSPECTION_ERROR_RESPONSE = { + "error": "invalid_request", + "error_description": "descrizione dell’errore", +} + +INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR = deepcopy(INTROSPECTION_ERROR_RESPONSE) +INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR.pop("error") + +INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR = deepcopy( + INTROSPECTION_ERROR_RESPONSE +) +INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR["error"] = "invalid_token" + +INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR = deepcopy(INTROSPECTION_ERROR_RESPONSE) +INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR.pop("error") + +INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR = deepcopy( + INTROSPECTION_ERROR_RESPONSE +) +INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR["error"] = "error" + +INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION = deepcopy( + INTROSPECTION_ERROR_RESPONSE +) +INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION.pop("error_description") + +INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy( + INTROSPECTION_ERROR_RESPONSE +) +INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION["error_description"] = [] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_request_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_request_settings.py new file mode 100644 index 0000000..270823f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_request_settings.py @@ -0,0 +1,32 @@ +from copy import deepcopy + +REVOCATION_REQUEST = { + "client_assertion": "eyJhbGciOiJIUzpXVCJ9.eyJzdWIdHJ1ZX0.LVyRDPVJm0S9q", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_id": "https://rp.spid.agid.gov.it", + "token": "eyJhbGciOiJSUzI1NiJ9.eyR9.FXDtEzDLbTHzFNroW7w27RLk", +} + +REVOCATION_REQUEST_NO_CLIENT_ASSERTION = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion") + +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = ".payload." + +REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type") + +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE["client_assertion_type"] = "" + +REVOCATION_REQUEST_NO_CLIENT_ID = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CLIENT_ID.pop("client_id") + +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID["client_id"] = "" + +REVOCATION_REQUEST_NO_TOKEN = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_TOKEN.pop("token") + +REVOCATION_REQUEST_NO_CORRECT_TOKEN = deepcopy(REVOCATION_REQUEST) +REVOCATION_REQUEST_NO_CORRECT_TOKEN["token"] = ".." diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_response_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_response_settings.py new file mode 100644 index 0000000..83a7f9f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/revocation_response_settings.py @@ -0,0 +1,20 @@ +from copy import deepcopy + +REVOCATION_RESPONSE_CIE = { + "error": "unauthorized_client", + "error_description": "description of the error", +} + +REVOCATION_RESPONSE_CIE_NO_ERROR = deepcopy(REVOCATION_RESPONSE_CIE) +REVOCATION_RESPONSE_CIE_NO_ERROR.pop("error") + +REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR = deepcopy(REVOCATION_RESPONSE_CIE) +REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR["error"] = [] + +REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION = deepcopy(REVOCATION_RESPONSE_CIE) +REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION.pop("error_description") + +REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy(REVOCATION_RESPONSE_CIE) +REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION["error_description"] = [ + "error description" +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/settings.py new file mode 100644 index 0000000..eeb4ee7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/settings.py @@ -0,0 +1,141 @@ +op_conf_priv_jwk = { + "use": "sig", + "kty": "RSA", + "kid": "dB67gL7ck3TFiIAf7N6_7SHvqk0MDYMEQcoGGlkUAAw", + "n": "01_4aI2Lu5ggsElmRkE_S_a83V_szXU0txV4db2hmJ8HR1Y2s7PsZZ5-emGpnTydGrR3n-QExeEEIcFt_a06Ryiink34RQcKoGXUDBMBU0Bu8G7NcZ99YX6yeG9wFi4xs-WviTPmtPqijkz6jm1_ltWDcwbktfkraIRKKggZaEl9ldtsFr2wSpin3AXuGIdeJ0hZqhF92ODBLGjJlaIL9KlwopDy56adReVnraawSdrxmuPGj78IEADNAme2nQNvv9UCu0FkAn5St1bKds3Gpv26W0kjr1gZLsmQrj9lTcDk_KbAwfEY__P7se62kusoSuKMTQqUG1TQpUY7oFGSdw", + "e": "AQAB", + "d": "AxvyR3dtisDeGjm6K2ZiS_fBJ3B1xz_mGptSPkkfy2LrdH0sKNCItaXzLlGpcQqnNPFaoRt1hoOcz_JMb-LQbKOIYNO8xycnXNyildrhthvIhfjbRMSXz4tRjLv25hyf1omWX2pIBnl5UqaHOPkbW1igqlx7mMGlhdAMVznvRb1oJOXxPtSdlVOhqT5ohn4BJZcJ85TKm_E49KUKUsz_xkd3BlYnUaiuEu-VQbC0u61iH-N017wg4ZQn9eXQgAzFdSNI2GQ7IH76l00iUu-y9oT2ld4WHGYMmsZBJgaQZnu2Yj4IMb5NjQz2VAOTU844RXP7-7Y5rWsS1oewfWSHAQ", + "p": "9dvm96S4wBrwzs73DOG8eV6sWEN13ZJNLa4tKzJvonOLG_8Z9Rshnfg_HcYDm_mqRD-nFC-8JKufnNaZbZuO_hDiA6UkvlZMGePxoN7nueHGfAqZ9lRQX0Vpp2Ac480jJgBLOhwgjkySRI5Ck-enTGabkxVN_LJSU9FC54TW6KE", + "q": "3BfwsumO62vhekd2gm97Uk1XFOM5UXmb1esdFnZ_RZRw1tDUB2irTxLJSqECCZbMWRoFRB3k-17LAOMaITUqv8g-83zdCmR3gpO71ahbdEfkGGKQYL7WmMNv3YvSMY5lN4_mCEz8R_OCz0_H7bip0Es_KAl5TSXjnVTvaACnLBc", +} + +op_conf = { + "sub": "http://op-test/oidc/op/", + "default_exp": 2880, + "default_signature_alg": "RS256", + "authority_hints": ["http://op-test/oidc/op/"], + "jwks_fed": [op_conf_priv_jwk], + "jwks_core": [op_conf_priv_jwk], + "trust_marks": [], + "trust_mark_issuers": {}, + "entity_type": "openid_provider", + "metadata": { + "openid_provider": { + "authorization_endpoint": "http://op-test/oidc/op/authorization/", + "token_endpoint": "http://op-test/oidc/op/token", + "userinfo_endpoint": "http://op-test/oidc/op/userinfo/", + "introspection_endpoint": "http://op-test/oidc/op/introspection/", + "revocation_endpoint": "http://op-test/oidc/op/revocation/", + "claims_parameter_supported": True, + "contacts": ["ops@op-test.it"], + "client_registration_types_supported": ["automatic"], + "request_authentication_methods_supported": {"ar": ["request_object"]}, + "acr_values_supported": [ + "https://www.spid.gov.it/SpidL1", + "https://www.spid.gov.it/SpidL2", + "https://www.spid.gov.it/SpidL3", + ], + "claims_supported": [ + "https://attributes.eid.gov.it/spid_code", + "given_name", + "family_name", + "place_of_birth", + "birthdate", + "gender", + "https://attributes.eid.gov.it/company_name", + "https://attributes.eid.gov.it/registered_office", + "https://attributes.eid.gov.it/fiscal_number", + "https://attributes.eid.gov.it/company_fiscal_number", + "https://attributes.eid.gov.it/vat_number", + "document_details", + "phone_number", + "email", + "https://attributes.eid.gov.it/e_delivery_service", + "https://attributes.eid.gov.it/eid_exp_date", + "address" + ], + "grant_types_supported": ["authorization_code", "refresh_token"], + "id_token_signing_alg_values_supported": ["RS256", "ES256"], + "issuer": "http://op-test/oidc/op/", + "jwks": { + "keys": [ + { + "use": "sig", + "kty": "RSA", + "n": "01_4aI2Lu5ggsElmRkE_S_a83V_szXU0txV4db2hmJ8HR1Y2s7PsZZ5-emGpnTydGrR3n-QExeEEIcFt_a06Ryiink34RQcKoGXUDBMBU0Bu8G7NcZ99YX6yeG9wFi4xs-WviTPmtPqijkz6jm1_ltWDcwbktfkraIRKKggZaEl9ldtsFr2wSpin3AXuGIdeJ0hZqhF92ODBLGjJlaIL9KlwopDy56adReVnraawSdrxmuPGj78IEADNAme2nQNvv9UCu0FkAn5St1bKds3Gpv26W0kjr1gZLsmQrj9lTcDk_KbAwfEY__P7se62kusoSuKMTQqUG1TQpUY7oFGSdw", + "e": "AQAB", + "kid": "dB67gL7ck3TFiIAf7N6_7SHvqk0MDYMEQcoGGlkUAAw", + } + ] + }, + "scopes_supported": ["openid", "offline_access"], + "logo_uri": "http://op-test/oidc/op/statics/logo.svg", + "organization_name": "SPID OIDC identity provider", + "op_policy_uri": "http://op-test/oidc/op/en/website/legal-information/", + "request_parameter_supported": True, + "request_uri_parameter_supported": True, + "require_request_uri_registration": True, + "response_types_supported": ["code"], + "subject_types_supported": ["pairwise", "public"], + "token_endpoint_auth_methods_supported": ["private_key_jwt"], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + ], + "userinfo_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW", + ], + "userinfo_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM", + ], + "userinfo_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + ], + "request_object_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW", + ], + "request_object_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM", + ], + "request_object_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512", + ], + } + }, + "constraints": {}, + "is_active": True, +} diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_02_authn_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_02_authn_endpoint.py new file mode 100644 index 0000000..d59a23e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_02_authn_endpoint.py @@ -0,0 +1,353 @@ +from copy import deepcopy +from django.contrib.auth import get_user_model +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import RP_METADATA, RP_METADATA_JWK1, rp_onboarding_data +from django.utils import timezone + +from spid_cie_oidc.entity.jwtse import create_jws, unpad_jwt_payload, verify_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain, +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import datetime_from_timestamp, exp_from_now, iat_now +from spid_cie_oidc.entity.utils import get_jwks +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk +from spid_cie_oidc.relying_party.utils import random_string + +RP_SUB = rp_onboarding_data["sub"] + +class AuthnRequestTest(TestCase): + def setUp(self): + + self.REQUEST_OBJECT_PAYLOAD = { + "client_id": RP_SUB, + "sub": RP_SUB, + "iss": RP_SUB, + "response_type": "code", + "scope": ["openid"], + "code_challenge": "qWJlMe0xdbXrKxTm72EpH659bUxAxw80", + "code_challenge_method": "S256", + "nonce": random_string(), + "prompt": "consent login", + "redirect_uri": f"{RP_SUB}callback/", + "acr_values": ["https://www.spid.gov.it/SpidL1", "https://www.spid.gov.it/SpidL2"], + "claims": { + "id_token": {"family_name": {"essential": True}, "email": {"essential": True}}, + "userinfo": {"given_name": None, "family_name": None, "email": None}, + }, + "state": random_string(), + "aud": ["https://op.spid.agid.gov.it/auth"], + "iat": iat_now(), + "exp": exp_from_now(), + "jti": "a72d5df0-2415-4c7c-a44f-3988b354040b", + } + + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + is_staff= False, + attributes={ + "family_name": "rossi", + "username": "unique_value", + "fiscal_number": "a7s6da87d6a87sd6as78d", + "email": "test@test.it", + }, + ) + self.user.set_password("test") + self.user.save() + self.op_conf = FederationEntityConfiguration.objects.create(**op_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + self.trust_chain = TrustChain.objects.create( + sub=RP_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=RP_METADATA, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_unknown_error(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + TrustChain.objects.all().delete() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 302) + self.assertTrue("error=invalid_request" in res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + @override_settings(OIDCFED_DEFAULT_PROVIDER_PROFILE="cie") + def test_auth_request_id_token_claim(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + consent_page_url = res.url + res = client.get(consent_page_url) + self.assertTrue("agree" in res.content.decode()) + self.assertFalse("error" in res.content.decode()) + res = client.post(consent_page_url, {"agree": True}) + issued_token = IssuedToken.objects.all().first() + verify_jws(issued_token.id_token, op_conf_priv_jwk) + id_token = unpad_jwt_payload(issued_token.id_token) + self.assertEqual(id_token.get("family_name"), "rossi") + self.assertEqual(id_token.get("email"), "test@test.it") + + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_no_request(self): + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {}) + self.assertTrue(res.status_code == 400) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_ok(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + consent_page_url = res.url + res = client.get(consent_page_url) + self.assertTrue("agree" in res.content.decode()) + self.assertFalse("error" in res.content.decode()) + res = client.post(consent_page_url, {"agree": True}) + self.assertTrue(res.status_code == 302) + self.assertTrue("code" in res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_user_rejected_consent(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + consent_page_url = res.url + res = client.get(consent_page_url) + self.assertTrue("agree" in res.content.decode()) + self.assertFalse("error" in res.content.decode()) + res = client.post(consent_page_url, {"agree": False}) + self.assertTrue(res.status_code == 302) + # TODO: this is not normative + self.assertTrue("error=rejected_by_user" in res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_no_session_in_post_consent(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + consent_page_url = res.url + res = client.get(consent_page_url) + self.assertTrue("agree" in res.content.decode()) + self.assertFalse("error" in res.content.decode()) + OidcSession.objects.all().delete() + res = client.post(consent_page_url, {"agree": True}) + self.assertTrue(res.status_code == 403) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_no_session_in_get_consent(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, + { + "username": "test", + "password": "test", + "authz_request_object": jws + } + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + consent_page_url = res.url + OidcSession.objects.all().delete() + res = client.get(consent_page_url) + self.assertTrue(res.status_code == 403) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_auth_code_already_used(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertFalse("error" in res.content.decode()) + self.assertTrue(res.status_code == 302) + session = OidcSession.objects.all().first() + IssuedToken.objects.create(session=session, expires=timezone.localtime()) + consent_page_url = res.url + res = client.get(consent_page_url) + self.assertTrue(res.status_code == 403) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_wrong_login(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "notest", "password": "test", "authz_request_object": jws} + ) + self.assertIn("error", res.content.decode()) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_preexistent_authz(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 200) + self.assertIn("username", res.content.decode()) + self.assertIn("password", res.content.decode()) + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 302) + self.assertIn("error=invalid_request", res.url) + self.assertIn("state", res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_trust_chain_no_active(self): + self.trust_chain.is_active = False + self.trust_chain.save() + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 302) + self.assertIn("error=invalid_request", res.url) + self.assertIn("state", res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_invalid_jwk(self): + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + get_jwks(self.trust_chain.metadata['openid_relying_party'])[0][ + "kid" + ] = "31ALfVXx9dcAMMHCVvh42qLTlanBL_r6BTnD5uMDzFT" + self.trust_chain.save() + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 302) + self.assertIn("error=invalid_request", res.url) + self.assertIn("state", res.url) + get_jwks(self.trust_chain.metadata['openid_relying_party'])[0][ + "kid" + ] = RP_METADATA_JWK1['kid'] + self.trust_chain.save() + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_no_correct_payload(self): + NO_CORRECT_OBJECT_PAYLOAD = deepcopy(self.REQUEST_OBJECT_PAYLOAD) + NO_CORRECT_OBJECT_PAYLOAD["response_type"] = "test" + jws = create_jws(NO_CORRECT_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 302) + self.assertIn("error", res.url) + self.assertIn("state", res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_invalid_session(self): + client = Client() + url = reverse("oidc_provider_consent") + res = client.get(url) + self.assertTrue(res.status_code == 403) + res = client.post(url) + self.assertTrue(res.status_code == 403) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_no_correct_refresh_request(self): + local_payload = deepcopy(self.REQUEST_OBJECT_PAYLOAD) + local_payload["scope"] = "openid offline_access" + local_payload["prompt"] = "login" + jws = create_jws(local_payload, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.get(url, {"request": jws}) + self.assertTrue(res.status_code == 403) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_user_staff(self): + self.user.is_staff = True + self.user.save() + jws = create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": jws} + ) + self.assertTrue(res.status_code == 302) + self.assertTrue("/oidc/op/rp-test/landing/" == res.url) + + @override_settings(OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_auth_request_no_correct_authz_request(self): + self.user.is_staff = True + self.user.save() + create_jws(self.REQUEST_OBJECT_PAYLOAD, RP_METADATA_JWK1) + client = Client() + url = reverse("oidc_provider_authnrequest") + res = client.post( + url, {"username": "test", "password": "test", "authz_request_object": {}} + ) + self.assertTrue(res.status_code == 403) + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_authn_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_authn_request.py new file mode 100644 index 0000000..60c3fd1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_authn_request.py @@ -0,0 +1,154 @@ +import logging + +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.schemas.authn_requests import ( + AuthenticationRequestCie, + AuthenticationRequestSpid, +) + +from .authn_request_settings import ( + AUTHN_REQUEST_CIE, + AUTHN_REQUEST_CIE_NO_ACR_VALUES, + AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES, + AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT, + AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE, + AUTHN_REQUEST_CIE_NO_PROMPT, + AUTHN_REQUEST_CIE_NO_SCOPE, + AUTHN_REQUEST_SPID, + AUTHN_REQUEST_SPID_NO_CLIENT_ID, + AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE, + AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD, + AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES, + AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS, + AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD, + AUTHN_REQUEST_SPID_NO_CORRECT_NONCE, + AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT, + AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL, + AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE, + AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE, + AUTHN_REQUEST_SPID_NO_CORRECT_STATE, + AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES, + AUTHN_REQUEST_SPID_NO_NONCE, + AUTHN_REQUEST_SPID_NO_REDIRECT_URL, + AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE, + AUTHN_REQUEST_SPID_NO_SCOPE, + AUTHN_REQUEST_SPID_NO_STATE, +) + +logger = logging.getLogger(__name__) + + +class AuthRequestTest(TestCase): + def test_validate_spid(self): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID) + + def test_validate_spid_no_client_id(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CLIENT_ID) + + def test_validate_spid_no_correct_claims(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_CLAIMS) + + def test_validate_spid_no_code_challenge(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE) + + def test_validate_spid_no_code_challenge_method(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CODE_CHALLENGE_METHOD) + + def test_validate_spid_no_correct_code_challenge_method(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid( + **AUTHN_REQUEST_SPID_NO_CORRECT_CODE_CHALLENGE_METHOD + ) + + def test_validate_spid_no_nonce(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_NONCE) + + def test_validate_spid_no_correct_nonce(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_NONCE) + + # removed, relaxed with the GAIN-PoC integration + # def test_validate_spid_no_prompt(self): + # with self.assertRaises(ValidationError): + # AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_PROMPT) + + def test_validate_spid_no_correct_prompt(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_PROMPT) + + def test_validate_spid_no_redirect_url(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_REDIRECT_URL) + + def test_validate_spid_no_correct_redirect_url(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_REDIRECT_URL) + + def test_validate_spid_no_response_type(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_RESPONSE_TYPE) + + def test_validate_spid_no_correct_response_type(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_RESPONSE_TYPE) + + def test_validate_spid_no_scope(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_SCOPE) + + def test_validate_spid_no_correct_scope(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_SCOPE) + +# def test_validate_spid_no_acr_values(self): +# with self.assertRaises(ValidationError): +# AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_ACR_VALUES) + + def test_validate_spid_no_correct_acr_values(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_ACR_VALUES) + + def test_validate_spid_no_state(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_STATE) + + def test_validate_spid_no_correct_state(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_STATE) + + def test_validate_spid_no_correct_ui_locales(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_SPID_NO_CORRECT_UI_LOCALES) + + def test_validate_cie(self): + AuthenticationRequestCie(**AUTHN_REQUEST_CIE) + + def test_validate_cie_no_acr_values(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_ACR_VALUES) + + def test_validate_cie_no_correct_acr_values(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_ACR_VALUES) + + def test_validate_cie_no_prompt(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_PROMPT) + + def test_validate_cie_no_correct_prompt(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_PROMPT) + + def test_validate_cie_no_scope(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_SCOPE) + + def test_validate_cie_no_correct_scope(self): + with self.assertRaises(ValidationError): + AuthenticationRequestSpid(**AUTHN_REQUEST_CIE_NO_CORRECT_SCOPE) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_refresh_token.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_refresh_token.py new file mode 100644 index 0000000..e664998 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_03_refresh_token.py @@ -0,0 +1,191 @@ +from copy import deepcopy +import time +from cryptojwt.jws.utils import left_hash +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from django.utils import timezone +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.authority.tests.settings import ( + RP_METADATA, + RP_METADATA_JWK1, + rp_conf +) +from spid_cie_oidc.entity.jwtse import create_jws, verify_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk + +RP_SUB = rp_conf["sub"] +RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"] + + +class RefreshTokenTest(TestCase): + + def setUp(self): + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + self.trust_chain = TrustChain.objects.create( + sub=RP_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + jwks=[], + metadata=RP_METADATA, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/token"] + CLIENT_ASSERTION = { + "iss": RP_SUB, + "sub": RP_SUB, + "aud": self.jwt_auds, + "exp": exp_from_now(), + "iat": iat_now(), + "jti": "jti", + } + self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1) + refresh_token = { + "iss": self.op_local_conf["sub"], + "sub": RP_SUB, + "aud": self.jwt_auds, + "client_id": RP_CLIENT_ID, + "scope": "openid", + } + self.rt_jws = create_jws(refresh_token, op_conf_priv_jwk) + id_token = { + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW', + 'at_hash': '', + 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"], ], + 'iss': op_conf["sub"], + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + access_token = { + 'iss': op_conf["sub"], + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"], ], + 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"], + 'scope': 'openid', + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + self.jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + id_token['at_hash'] = left_hash(self.jwt_at, "HS256") + self.jwt_id = create_jws(id_token, op_conf_priv_jwk) + + session = OidcSession.objects.create( + user=User.objects.create(username="username"), + user_uid="", + nonce="", + authz_request={"scope": "offline_access", "prompt": "consent", "nonce": "123", + "acr_values": ["https://www.spid.gov.it/SpidL2", "https://www.spid.gov.it/SpidL1"]}, + client_id=RP_SUB, + auth_code="code", + ) + IssuedToken.objects.create( + refresh_token=self.rt_jws, + id_token=self.jwt_id, + access_token=self.jwt_at, + session=session, + expires=timezone.localtime() + ) + self.assertEqual(refresh_token["iss"], self.op_local_conf["sub"]) + + @override_settings(OIDCFED_PROVIDER_PROFILE="spid") + def test_grant_refresh_token_spid(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=self.rt_jws, + grant_type="refresh_token" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + refresh_token = verify_jws(res.json().get("refresh_token"), op_conf_priv_jwk) + self.assertEqual(refresh_token["aud"], RP_CLIENT_ID) + self.assertEqual(refresh_token["iss"], self.op_local_conf["sub"]) + + @override_settings(OIDCFED_PROVIDER_PROFILE="cie") + def test_grant_refresh_token_cie(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=self.rt_jws, + grant_type="refresh_token" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + refresh_token = verify_jws(res.json().get("refresh_token"), op_conf_priv_jwk) + self.assertEqual(refresh_token["aud"], RP_CLIENT_ID) + + @override_settings(OIDCFED_PROVIDER_MAX_REFRESH=1, OIDCFED_PROVIDER_PROFILE="spid") + def test_grant_refresh_token_two_times_spid(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=self.rt_jws, + grant_type="refresh_token" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=res.json()["refresh_token"], + grant_type="refresh_token" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 400) + + @override_settings(OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME=1, OIDCFED_PROVIDER_PROFILE="cie") + def test_grant_refresh_token_two_times_cie(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=self.rt_jws, + grant_type="refresh_token" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + request = dict( + client_id=RP_CLIENT_ID, + client_assertion=self.ca_jws, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + refresh_token=res.json()["refresh_token"], + grant_type="refresh_token" + ) + time.sleep(2) + res = client.post(url, request) + self.assertTrue(res.status_code == 400) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_authn_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_authn_response.py new file mode 100644 index 0000000..6a616c1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_authn_response.py @@ -0,0 +1,83 @@ +import logging + +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.schemas.authn_response import ( + AuthenticationErrorResponse, + AuthenticationErrorResponseCie, + AuthenticationResponse, + AuthenticationResponseCie, +) + +from .authn_responses_settings import ( + AUTHN_ERROR_RESPONSE_CIE, + AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS, + AUTHN_ERROR_RESPONSE_CIE_NO_ISS, + AUTHN_ERROR_RESPONSE_SPID, + AUTHN_ERROR_RESPONSE_SPID_NO_CODE, + AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE, + AUTHN_ERROR_RESPONSE_SPID_NO_STATE, + AUTHN_RESPONSE_CIE, + AUTHN_RESPONSE_CIE_NO_CORRECT_ISS, + AUTHN_RESPONSE_CIE_NO_ISS, + AUTHN_RESPONSE_SPID, + AUTHN_RESPONSE_SPID_NO_CODE, + AUTHN_RESPONSE_SPID_NO_CORRECT_STATE, + AUTHN_RESPONSE_SPID_NO_STATE, +) + +logger = logging.getLogger(__name__) + + +class AuthResponseTest(TestCase): + def test_validate_response_spid(self): + AuthenticationResponse(**AUTHN_RESPONSE_SPID) + + def test_validate_response_spid_no_code(self): + with self.assertRaises(ValidationError): + AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_CODE) + + def test_validate_response_spid_no_state(self): + with self.assertRaises(ValidationError): + AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_STATE) + + def test_validate_response_spid_no_correct_state(self): + with self.assertRaises(ValidationError): + AuthenticationResponse(**AUTHN_RESPONSE_SPID_NO_CORRECT_STATE) + + def test_validate_response_cie(self): + AuthenticationResponseCie(**AUTHN_RESPONSE_CIE) + + def test_validate_response_cie_no_iss(self): + with self.assertRaises(ValidationError): + AuthenticationResponseCie(**AUTHN_RESPONSE_CIE_NO_ISS) + + def test_validate_response_cie_no_correct_iss(self): + with self.assertRaises(ValidationError): + AuthenticationResponseCie(**AUTHN_RESPONSE_CIE_NO_CORRECT_ISS) + + def test_validate_error_response_spid(self): + AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID) + + def test_validate_error_response_spid_no_code(self): + with self.assertRaises(ValidationError): + AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_CODE) + + def test_validate_error_response_spid_no_state(self): + with self.assertRaises(ValidationError): + AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_STATE) + + def test_validate_error_response_spid_no_correct_state(self): + with self.assertRaises(ValidationError): + AuthenticationErrorResponse(**AUTHN_ERROR_RESPONSE_SPID_NO_CORRECT_STATE) + + def test_validate_error_response_cie(self): + AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE) + + def test_validate_error_response_spid_no_iss(self): + with self.assertRaises(ValidationError): + AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE_NO_ISS) + + def test_validate_error_response_spid_no_correct_iss(self): + with self.assertRaises(ValidationError): + AuthenticationErrorResponseCie(**AUTHN_ERROR_RESPONSE_CIE_NO_CORRECT_ISS) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_userinfo_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_userinfo_endpoint.py new file mode 100644 index 0000000..1114d09 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_04_userinfo_endpoint.py @@ -0,0 +1,111 @@ +from copy import deepcopy + +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.authority.tests.settings import ( + RP_METADATA, + rp_onboarding_data +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now + ) +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk + + +class UserInfoEndpointTest(TestCase): + + def setUp(self): + self.RP_SUB = rp_onboarding_data["sub"] + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + + def define_db(self): + session = OidcSession.objects.create( + user=User.objects.create( + username = "username", attributes = {"email" : "test@test.it"} + ), + user_uid="", + nonce="", + authz_request={ + "scope": "openid", "nonce": "123", "claims":{ + "userinfo":{"email": None} + } + }, + client_id=self.RP_SUB, + ) + + access_token = { + "iss": self.RP_SUB, + "sub": op_conf["sub"], + "aud": [self.RP_SUB], + "client_id": self.RP_SUB, + "scope": "openid", + } + jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + iss_token_data = dict( + session=session, + access_token= jwt_at, + id_token= "id_token", + expires=datetime_from_timestamp(exp_from_now()) + ) + IssuedToken.objects.create(**iss_token_data) + headers= { + "HTTP_AUTHORIZATION": f"Bearer {jwt_at}" + } + return headers + + def test_userinfo_endpoint_no_header(self): + client = Client() + url = reverse("oidc_provider_userinfo_endpoint") + res = client.get(url, {}) + self.assertTrue(res.status_code == 403) + + def test_userinfo_endpoint_no_issued_token_session(self): + client = Client() + url = reverse("oidc_provider_userinfo_endpoint") + headers= { + "HTTP_AUTHORIZATION": "Bearer dC34Pf6kdG" + } + res = client.get(url, data = {}, **headers) + self.assertTrue(res.status_code == 403) + + def test_userinfo_endpoint_no_tc(self): + headers = self.define_db() + client = Client() + url = reverse("oidc_provider_userinfo_endpoint") + res = client.get(url, data = {}, **headers) + self.assertTrue(res.status_code == 403) + + def test_userinfo_endpoint_ok(self): + headers = self.define_db() + self.trust_chain = TrustChain.objects.create( + sub=self.RP_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=RP_METADATA, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + client = Client() + url = reverse("oidc_provider_userinfo_endpoint") + res = client.get(url, data = {}, **headers) + self.assertTrue(res.status_code == 200) + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_introspection_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_introspection_endpoint.py new file mode 100644 index 0000000..437e0c1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_introspection_endpoint.py @@ -0,0 +1,124 @@ +from copy import deepcopy + +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.authority.tests.settings import ( + RP_METADATA, + rp_conf +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import ( + op_conf, + op_conf_priv_jwk +) + + +class IntrospectionEndpointTest(TestCase): + + def setUp(self): + self.RP_SUB = rp_conf["sub"] + self.RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"] + self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/introspection"] + CLIENT_ASSERTION = { + "iss": self.RP_SUB, + "sub": self.RP_SUB, + "aud": self.jwt_auds, + "exp": exp_from_now(), + "iat": iat_now(), + "jti": "jti", + } + self.ca_jws = create_jws(CLIENT_ASSERTION, rp_conf["jwks_core"][0]) + token = { + "iss": self.RP_SUB, + "sub": op_conf["sub"], + "aud": self.jwt_auds, + "client_id": self.RP_SUB, + "scope": "openid", + } + self.jwt_token = create_jws(token, op_conf_priv_jwk, typ="at+jwt") + session = OidcSession.objects.create( + user=User.objects.create(username = "username"), + user_uid="", + nonce="", + authz_request={"scope": "openid"}, + client_id=self.RP_SUB, + ) + iss_token_data = dict( + session=session, + access_token= self.jwt_token, + id_token= "id_token", + refresh_token= "refresh_token", + expires=datetime_from_timestamp(exp_from_now()) + ) + IssuedToken.objects.create(**iss_token_data) + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + self.trust_chain = TrustChain.objects.create( + sub=self.RP_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=RP_METADATA, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + + def test_introspection_endpoint(self): + client = Client() + url = reverse("oidc_provider_introspection_endpoint") + request = { + "client_assertion" : self.ca_jws, + "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_id" : self.RP_SUB, + "token" : self.jwt_token + + } + + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + self.assertTrue("openid" in res.content.decode()) + + def test_introspection_endpoint_get(self): + client = Client() + url = reverse("oidc_provider_introspection_endpoint") + request = { + "client_assertion" : self.ca_jws, + "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "client_id" : self.RP_SUB, + "token" : self.jwt_token + + } + res = client.get(url, request) + self.assertTrue(res.status_code == 400) + + def test_introspection_endpoint_validation_error(self): + client = Client() + url = reverse("oidc_provider_introspection_endpoint") + request = { + "client_assertion" : self.ca_jws, + "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + "token" : self.jwt_token + + } + res = client.post(url, request) + self.assertTrue(res.status_code == 400) + self.assertTrue(res.json()["error"] == "invalid_request") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_token_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_token_request.py new file mode 100644 index 0000000..c19c0a5 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_05_token_request.py @@ -0,0 +1,147 @@ +import logging + +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.schemas.jwt import JwtStructure +from spid_cie_oidc.provider.schemas.token_requests import ( + TokenAuthnCodeRequest, + TokenRefreshRequest, +) + +from .token_request_settings import ( + JWT_CLIENT_ASSERTION, + JWT_CLIENT_ASSERTION_NO_AUD, + JWT_CLIENT_ASSERTION_NO_CORRECT_AUD, + JWT_CLIENT_ASSERTION_NO_CORRECT_EXP, + JWT_CLIENT_ASSERTION_NO_CORRECT_IAT, + JWT_CLIENT_ASSERTION_NO_CORRECT_ISS, + JWT_CLIENT_ASSERTION_NO_CORRECT_JTI, + JWT_CLIENT_ASSERTION_NO_CORRECT_SUB, + JWT_CLIENT_ASSERTION_NO_EXP, + JWT_CLIENT_ASSERTION_NO_IAT, + JWT_CLIENT_ASSERTION_NO_ISS, + JWT_CLIENT_ASSERTION_NO_JTI, + JWT_CLIENT_ASSERTION_NO_SUB, + TOKEN_AUTHN_CODE_REQUEST, + TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION, + TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE, + TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID, + TOKEN_AUTHN_CODE_REQUEST_NO_CODE, + TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY, + TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION, + TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE, + TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE, + TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE, + TOKEN_REFRESH_REQUEST, + TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE, + TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE, +) + +logger = logging.getLogger(__name__) + + +class TokenRequestTest(TestCase): + def test_validate_request(self): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST) + + def test_validate_request_no_client(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID) + + def test_validate_request_no_client_assertion(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION) + + def test_validate_request_no_correct_client_assertion(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest( + **TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION + ) + + def test_validate_request_no_client_assertion_type(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE) + + def test_validate_request_no_correct_client_assertion_type(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest( + **TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE + ) + + def test_validate_request_no_code(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CODE) + + def test_validate_request_no_code_verify(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY) + + def test_validate_request_no_grant_type(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE) + + def test_validate_request_no_correct_grant_type(self): + with self.assertRaises(ValidationError): + TokenAuthnCodeRequest(**TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE) + + def test_validate_refresh_request(self): + TokenRefreshRequest(**TOKEN_REFRESH_REQUEST) + + def test_validate_refresh_request_no_grant_type(self): + with self.assertRaises(ValidationError): + TokenRefreshRequest(**TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE) + + def test_validate_refresh_request_no_correct_grant_type(self): + with self.assertRaises(ValidationError): + TokenRefreshRequest(**TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE) + + def test_validate_jwt(self): + JwtStructure(**JWT_CLIENT_ASSERTION) + + def test_validate_jwt_no_iss(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_ISS) + + def test_validate_jwt_no_correct_iss(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_ISS) + + def test_validate_jwt_no_sub(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_SUB) + + def test_validate_jwt_no_correct_sub(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_SUB) + + def test_validate_jwt_no_iat(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_IAT) + + def test_validate_jwt_no_correct_iat(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_IAT) + + def test_validate_jwt_no_exp(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_EXP) + + def test_validate_jwt_no_correct_exp(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_EXP) + + def test_validate_jwt_no_jti(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_JTI) + + def test_validate_jwt_no_correct_jti(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_JTI) + + def test_validate_jwt_no_aud(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_AUD) + + def test_validate_jwt_no_correct_aud(self): + with self.assertRaises(ValidationError): + JwtStructure(**JWT_CLIENT_ASSERTION_NO_CORRECT_AUD) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_processors.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_processors.py new file mode 100644 index 0000000..ae31027 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_processors.py @@ -0,0 +1,11 @@ + + +from django.test import TestCase +from spid_cie_oidc.provider.processors import spidCode + +class ProcessorsTest(TestCase): + + def test_processors(self): + processed = "9893261da4a1fec47a19b2cc4237d82f3eb70ddafdaad51bb61138b5126e72644c3189db53f9303cdc85cb84438caf9c07431944894b5742a1d33d111abf88c8" + res = spidCode({"username" : "testusername"}, {}) + self.assertEqual(res, processed) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_token_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_token_response.py new file mode 100644 index 0000000..1e2d2ea --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_06_token_response.py @@ -0,0 +1,77 @@ +import logging + +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.tests.token_response_settings import ( + TOKEN_ERROR_RESPONSE, + TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR, + TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION, + TOKEN_ERROR_RESPONSE_NO_ERROR, + TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION, + TOKEN_REFRESH_RESPONSE, + TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN, + TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN, + TOKEN_RESPONSE, + TOKEN_RESPONSE_NO_ACCESS_TOKEN, + TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN, + TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE, + TOKEN_RESPONSE_NO_TOKEN_TYPE, +) +from spid_cie_oidc.provider.schemas.token_response import ( + TokenErrorResponse, + TokenRefreshResponse, + TokenResponse, +) + +logger = logging.getLogger(__name__) + + +class TokenResponseTest(TestCase): + def test_validate_token_response(self): + TokenResponse(**TOKEN_RESPONSE) + + def test_validate_token_response(self): + TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE) + + def test_validate_token_response_no_access_token(self): + with self.assertRaises(ValidationError): + TokenResponse(**TOKEN_RESPONSE_NO_ACCESS_TOKEN) + + def test_validate_token_response_no_correct_access_token(self): + with self.assertRaises(ValidationError): + TokenResponse(**TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN) + + def test_validate_token_response_no_token_type(self): + with self.assertRaises(ValidationError): + TokenResponse(**TOKEN_RESPONSE_NO_TOKEN_TYPE) + + def test_validate_token_response_no_correct_token_type(self): + with self.assertRaises(ValidationError): + TokenResponse(**TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE) + + def test_validate_token_refresh_response_no_refresh_token(self): + with self.assertRaises(ValidationError): + TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN) + + def test_validate_token_refresh_response_no_correct_refresh_token(self): + with self.assertRaises(ValidationError): + TokenRefreshResponse(**TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN) + + def test_validate_token_error_response(self): + TokenErrorResponse(**TOKEN_ERROR_RESPONSE) + + def test_validate_token_error_no_error(self): + with self.assertRaises(ValidationError): + TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_ERROR) + + def test_validate_token_error_no_correct_error(self): + with self.assertRaises(ValidationError): + TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR) + + def test_validate_token_error_no_error_description(self): + with self.assertRaises(ValidationError): + TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION) + + def test_validate_token_error_no_correct_error_description(self): + with self.assertRaises(ValidationError): + TokenErrorResponse(**TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_fetch_relying_parties.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_fetch_relying_parties.py new file mode 100644 index 0000000..007ab0c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_fetch_relying_parties.py @@ -0,0 +1,89 @@ + + +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase, override_settings +from spid_cie_oidc.authority.tests.settings import ( + RP_CONF_AS_JSON, + RP_METADATA_JWK1 +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import ( + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import ( + TA_JWK_PRIVATE, + TA_SUB, + ta_conf_data_as_json +) +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) + +EXP = datetime_from_timestamp(exp_from_now(33)) +NOW = datetime_from_timestamp(iat_now()) + +def create_entity_config(): + JWS_RP = create_jws(RP_CONF_AS_JSON, RP_METADATA_JWK1, typ="entity-statement+jwt") + JWS_TA = create_jws(ta_conf_data_as_json, TA_JWK_PRIVATE, typ="entity-statement+jwt") + return [JWS_RP, JWS_TA] + +def create_tc(): + ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW + ) + return TrustChain.objects.create( + sub=RP_CONF_AS_JSON["sub"], + exp=EXP, + jwks = [], + metadata=RP_CONF_AS_JSON["metadata"], + status="valid", + trust_anchor=ta_fes, + is_active=True, + ) +class FetchRPTest(TestCase): + + def setUp(self): + + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + + def exec(self, cmd_name:str, *args, **kwargs): + call_command( + cmd_name, + *args, + **kwargs, + ) + + @override_settings(OIDCFED_IDENTITY_PROVIDERS = {"spid":{"http://127.0.0.1:8000/oidc/op/" :"http://testserver/"}, "cie":{}}) + @override_settings(OIDCFED_TRUST_ANCHOR = []) + def test_fetch_rp(self): + self.patcher = patch( + "spid_cie_oidc.entity.statements.get_entity_configurations", + return_value = create_entity_config() + ) + self.patcher.start() + self.patcher = patch( + "spid_cie_oidc.entity.statements.get_http_url", + return_value = ['["http://rp-test.it/oidc/rp/"]'] + ) + self.patcher.start() + self.patcher = patch( + "spid_cie_oidc.entity.trust_chain_operations.get_or_create_trust_chain", + return_value = create_tc() + ) + self.patcher.start() + self.exec('fetch_openid_relying_parties', '--from', 'url') + self.patcher.stop() + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_introspection_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_introspection_request.py new file mode 100644 index 0000000..fb53995 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_07_introspection_request.py @@ -0,0 +1,53 @@ +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.tests.introspection_request_settings import ( + INTROSPECTION_REQUEST, + INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION, + INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE, + INTROSPECTION_REQUEST_NO_CLIENT_ID, + INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION, + INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE, + INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID, + INTROSPECTION_REQUEST_NO_CORRECT_TOKEN, + INTROSPECTION_REQUEST_NO_TOKEN, +) +from spid_cie_oidc.provider.schemas.introspection_request import IntrospectionRequest + + +class IntrospectionRequestTest(TestCase): + def test_validate_introspection_request(self): + IntrospectionRequest(**INTROSPECTION_REQUEST) + + def test_validate_introspection_request_no_client_assertion(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION) + + def test_validate_introspection_request_no_correct_client_assertion(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION) + + def test_validate_introspection_request_no_client_assertion_type(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ASSERTION_TYPE) + + def test_validate_introspection_request_no_correct_client_assertion_type(self): + with self.assertRaises(ValidationError): + IntrospectionRequest( + **INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE + ) + + def test_validate_introspection_request_no_client_id(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CLIENT_ID) + + def test_validate_introspection_request_no_correct_client_id(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_CLIENT_ID) + + def test_validate_introspection_request_no_token(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_TOKEN) + + def test_validate_introspection_request_no_correct_token(self): + with self.assertRaises(ValidationError): + IntrospectionRequest(**INTROSPECTION_REQUEST_NO_CORRECT_TOKEN) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_introspection_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_introspection_response.py new file mode 100644 index 0000000..68973a9 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_introspection_response.py @@ -0,0 +1,68 @@ +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.tests.introspection_response_settings import ( + INTROSPECTION_ERROR_RESPONSE, + INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR, + INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR, + INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION, + INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION, + INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR, + INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR, + INTROSPECTION_RESPONSE, + INTROSPECTION_RESPONSE_NO_ACTIVE, + INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE, +) +from spid_cie_oidc.provider.schemas.introspection_response import ( + IntrospectionErrorResponse, + IntrospectionErrorResponseCie, + IntrospectionErrorResponseSpid, + IntrospectionResponse, +) + + +class IntrospectionResponseTest(TestCase): + def test_validate_introspection_response(self): + IntrospectionResponse(**INTROSPECTION_RESPONSE) + + def test_validate_introspection_response_no_active(self): + with self.assertRaises(ValidationError): + IntrospectionResponse(**INTROSPECTION_RESPONSE_NO_ACTIVE) + + def test_validate_introspection_response_no_correct_active(self): + with self.assertRaises(ValidationError): + IntrospectionResponse(**INTROSPECTION_RESPONSE_NO_CORRECT_ACTIVE) + + def test_validate_introspection_error_response(self): + IntrospectionErrorResponse(**INTROSPECTION_ERROR_RESPONSE) + + def test_validate_introspection_response_spid_no_error(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponseSpid(**INTROSPECTION_ERROR_RESPONSE_SPID_NO_ERROR) + + def test_validate_introspection_response_spid_no_correct_error(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponseSpid( + **INTROSPECTION_ERROR_RESPONSE_SPID_NO_CORRECT_ERROR + ) + + def test_validate_introspection_response_cie_no_error(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponseCie(**INTROSPECTION_ERROR_RESPONSE_CIE_NO_ERROR) + + def test_validate_introspection_response_cie_no_correct_error(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponseCie( + **INTROSPECTION_ERROR_RESPONSE_CIE_NO_CORRECT_ERROR + ) + + def test_validate_introspection_response_no_error_description(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponse( + **INTROSPECTION_ERROR_RESPONSE_NO_ERROR_DESCRIPTION + ) + + def test_validate_introspection_response_no_correct_error_description(self): + with self.assertRaises(ValidationError): + IntrospectionErrorResponse( + **INTROSPECTION_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_token_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_token_endpoint.py new file mode 100644 index 0000000..af68e2d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_08_token_endpoint.py @@ -0,0 +1,145 @@ +from copy import deepcopy + +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.authority.tests.settings import ( + RP_METADATA, + RP_METADATA_JWK1, + rp_conf +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk +from spid_cie_oidc.relying_party.utils import get_pkce + + +RP_SUB = rp_conf["sub"] +RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"] +PKCE = get_pkce() + +class RefreshTokenTest(TestCase): + + def setUp(self): + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + self.trust_chain = TrustChain.objects.create( + sub=RP_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=RP_METADATA, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/token"] + CLIENT_ASSERTION = { + "iss": RP_SUB, + "sub": RP_SUB, + "aud": self.jwt_auds, + "exp": exp_from_now(), + "iat": iat_now(), + "jti": "jti", + } + self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1) + + self.refresh_token = { + "iss": self.op_local_conf["sub"], + "sub": RP_SUB, + "exp": exp_from_now(), + "iat": iat_now(), + "aud": self.jwt_auds, + "client_id": RP_CLIENT_ID, + "scope": "openid", + } + session = OidcSession.objects.create( + user=User.objects.create(username = "username"), + user_uid="", + nonce="", + authz_request={"scope": "openid", "nonce": "123", "code_challenge": PKCE["code_challenge"]}, + client_id="", + auth_code="code", + ) + IssuedToken.objects.create( + access_token = create_jws(self.refresh_token, op_conf_priv_jwk, typ="at-jwt"), + refresh_token = create_jws(self.refresh_token, op_conf_priv_jwk), + session = session, + expires = timezone.localtime() + ) + + def test_token_endpoint(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + + request = dict( + client_id = RP_CLIENT_ID, + client_assertion = self.ca_jws, + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + grant_type="authorization_code", + code="code", + code_verifier = PKCE["code_verifier"] + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 200) + self.assertTrue(res.json().get("access_token")) + + def test_token_endpoint_get(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id = RP_CLIENT_ID, + client_assertion = self.ca_jws, + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + grant_type="authorization_code", + code = "code", + code_verifier = "code_verifier" + ) + res = client.get(url, request) + self.assertTrue(res.status_code == 400) + + def test_token_endpoint_no_client_id(self): + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_assertion = self.ca_jws, + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + grant_type="authorization_code", + code = "code", + code_verifier = "code_verifier" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 400) + self.assertTrue("invalid_request" == res.json().get("error")) + + def test_token_endpoint_no_session(self): + OidcSession.objects.all().delete() + client = Client() + url = reverse("oidc_provider_token_endpoint") + request = dict( + client_id = RP_CLIENT_ID, + client_assertion = self.ca_jws, + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + grant_type="authorization_code", + code = "code", + code_verifier = "code_verifier" + ) + res = client.post(url, request) + self.assertTrue(res.status_code == 400) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_endpoint.py new file mode 100644 index 0000000..eb14182 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_endpoint.py @@ -0,0 +1,110 @@ + +from copy import deepcopy + +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.authority.tests.settings import ( + RP_CONF_AS_JSON, + RP_METADATA_JWK1, + rp_conf +) +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain +) +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk + +RP_SUB = rp_conf["sub"] +RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"] + +class RevocationEndponitTest(TestCase): + + def setUp(self): + ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + self.jwt_auds = [op_conf["sub"], "http://testserver/oidc/op/", "http://testserver/oidc/op/revocation"] + CLIENT_ASSERTION = { + "iss": RP_SUB, + "sub": RP_SUB, + "aud": self.jwt_auds, + "exp": exp_from_now(), + "iat": iat_now(), + "jti": "jti", + } + self.ca_jws = create_jws(CLIENT_ASSERTION, RP_METADATA_JWK1) + access_token = { + "iss": self.op_local_conf["sub"], + "sub": RP_SUB, + "aud": self.jwt_auds, + "client_id": RP_CLIENT_ID, + "scope": "openid", + } + self.at_jws = create_jws(access_token, op_conf_priv_jwk) + self.request = dict( + client_id = RP_CLIENT_ID, + client_assertion = self.ca_jws, + client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + token = self.at_jws, + ) + session = OidcSession.objects.create( + user=User.objects.create(username = "username"), + user_uid="", + nonce="", + authz_request={"scope": "openid"}, + client_id=rp_conf["sub"], + ) + iss_token_data = dict( + session=session, + access_token= self.at_jws, + id_token= "id_token", + refresh_token= "refresh_token", + expires=datetime_from_timestamp(exp_from_now()) + ) + IssuedToken.objects.create(**iss_token_data) + + TrustChain.objects.create( + sub=RP_CONF_AS_JSON["sub"], + exp=datetime_from_timestamp(exp_from_now(33)), + jwks = [], + metadata=RP_CONF_AS_JSON["metadata"], + status="valid", + trust_anchor=ta_fes, + is_active=True, + ) + + def test_revocation_endpoint(self): + client = Client() + url = reverse("oidc_provider_end_session_endpoint") + res = client.post(url, self.request) + self.assertTrue(res.status_code == 200) + + def test_revocation_endpoint_no_correct_client_assert(self): + client = Client() + request = deepcopy(self.request) + request["client_assertion"] = "q.q.q" + url = reverse("oidc_provider_end_session_endpoint") + res = client.post(url, request) + self.assertTrue(res.status_code == 400) + + def test_revocation_endpoint_no_issued_token(self): + client = Client() + IssuedToken.objects.all().delete() + url = reverse("oidc_provider_end_session_endpoint") + res = client.post(url, self.request) + self.assertTrue(res.status_code == 400) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_request.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_request.py new file mode 100644 index 0000000..2ed47ef --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_09_revocation_request.py @@ -0,0 +1,62 @@ +from copy import deepcopy +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.tests.revocation_request_settings import ( + REVOCATION_REQUEST, + REVOCATION_REQUEST_NO_CLIENT_ASSERTION, + REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE, + REVOCATION_REQUEST_NO_CLIENT_ID, + REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION, + REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE, + REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID, + REVOCATION_REQUEST_NO_CORRECT_TOKEN, + REVOCATION_REQUEST_NO_TOKEN, +) +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration +) +from spid_cie_oidc.provider.schemas.revocation_request import RevocationRequest +from spid_cie_oidc.provider.tests.settings import ( + op_conf +) + +class RevocationRequestTest(TestCase): + + def setUp(self): + self.op_local_conf = deepcopy(op_conf) + FederationEntityConfiguration.objects.create(**self.op_local_conf) + + def test_validate_revocation_request(self): + RevocationRequest(**REVOCATION_REQUEST) + + def test_validate_revocation_request_no_client_assertion(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ASSERTION) + + def test_validate_revocation_request_no_correct_client_assertion(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION) + + def test_validate_revocation_request_no_client_assertion_type(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ASSERTION_TYPE) + + def test_validate_revocation_request_no_correct_client_assertion_type(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE) + + def test_validate_revocation_request_no_client_id(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CLIENT_ID) + + def test_validate_revocation_request_no_correct_client_id(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_CLIENT_ID) + + def test_validate_revocation_request_no_token(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_TOKEN) + + def test_validate_revocation_request_no_correct_token(self): + with self.assertRaises(ValidationError): + RevocationRequest(**REVOCATION_REQUEST_NO_CORRECT_TOKEN) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_no_consent.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_no_consent.py new file mode 100644 index 0000000..82f3113 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_no_consent.py @@ -0,0 +1,18 @@ + +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import rp_conf + + +class NoConsentTest(TestCase): + + def setUp(self): + pass + + def test_no_consent(self): + client = Client() + url = reverse("oidc_provider_not_consent") + redirect_uri = rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0] + res = client.get(url + "?redirect_uri=" + redirect_uri) + self.assertTrue(res.status_code == 302) + self.assertTrue("Authentication+request+rejected+by+user" in res.url) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_revocation_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_revocation_response.py new file mode 100644 index 0000000..de4478b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_10_revocation_response.py @@ -0,0 +1,35 @@ +from django.test import TestCase +from pydantic import ValidationError +from spid_cie_oidc.provider.tests.revocation_response_settings import ( + REVOCATION_RESPONSE_CIE, + REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR, + REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION, + REVOCATION_RESPONSE_CIE_NO_ERROR, + REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION, +) +from spid_cie_oidc.provider.schemas.revocation_response import ( + RevocationErrorResponse, +) + + +class RevocationResponseTest(TestCase): + def test_validate_revocation_response(self): + RevocationErrorResponse(**REVOCATION_RESPONSE_CIE) + + def test_validate_revocation_response_no_error(self): + with self.assertRaises(ValidationError): + RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_ERROR) + + def test_validate_revocation_response_no_correct_error(self): + with self.assertRaises(ValidationError): + RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR) + + def test_validate_revocation_response_no_error_description(self): + with self.assertRaises(ValidationError): + RevocationErrorResponse(**REVOCATION_RESPONSE_CIE_NO_ERROR_DESCRIPTION) + + def test_validate_revocation_response_no_correct_error_description(self): + with self.assertRaises(ValidationError): + RevocationErrorResponse( + **REVOCATION_RESPONSE_CIE_NO_CORRECT_ERROR_DESCRIPTION + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_11_user_access_history.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_11_user_access_history.py new file mode 100644 index 0000000..86975f2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_11_user_access_history.py @@ -0,0 +1,70 @@ +from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.provider.models import OidcSession + + +class UserAccessHistoryTest(TestCase): + + def setUp(self): + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + is_staff=True + ) + self.user.set_password("test") + self.user.save() + OidcSession.objects.create( + user=self.user, + user_uid="uid", + nonce="", + authz_request={ + "scope": "openid", + "nonce": "123", + "acr_values": ["https://www.spid.gov.it/SpidL2"], + "redirect_uri": rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0], + "state": "state", + }, + client_id="", + auth_code="auth_code", + ) + OidcSession.objects.create( + user=self.user, + user_uid="uid", + nonce="1234", + authz_request={ + "scope": "openid", + "nonce": "1234", + "acr_values": ["https://www.spid.gov.it/SpidL2"], + "redirect_uri": rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0], + "state": "state", + }, + client_id="", + auth_code="code", + ) + + def test_user_access_history(self): + client = Client() + client.login(username="test", password="test") + session = client.session + session.update({"oidc": {"auth_code": "code"}, "user_uid": "uid"}) + session.save() + url = reverse("oidc_provider_access_history") + res = client.get(url) + self.assertTrue(res.status_code == 200) + self.assertTrue("auth_code=auth_code" in res.content.decode()) + + def test_user_access_history_revoke(self): + client = Client() + client.login(username="test", password="test") + session = client.session + session.update({"oidc": {"auth_code": "code"}, "user_uid": "uid"}) + session.save() + url = reverse("oidc_provider_revoke_session") + res = client.get(url, {"auth_code": "auth_code"}) + self.assertTrue(res.status_code == 302) + url_history = reverse("oidc_provider_access_history") + self.assertTrue(url_history in res.url) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_12_init.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_12_init.py new file mode 100644 index 0000000..fcc1b3d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/test_12_init.py @@ -0,0 +1,36 @@ + +from django.test import TestCase +from spid_cie_oidc.provider.models import OidcSession +from spid_cie_oidc.provider.views.__init__ import OpBase + + +class InitTest(TestCase): + + def setUp(self): + self.client_assertion = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjJIbm9GUzNZbkM5dGppQ2FpdmhXTFZVSjNBeHdHR3pfOTh1UkZhcU1FRXMifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJzdWIiOiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvb2lkYy9ycC8iLCJhdWQiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDAwL29pZGMvb3AvdG9rZW4vIl0sImlhdCI6MTY0ODQ5NTc1NywiZXhwIjoxNjQ4NDk3NzM3LCJqdGkiOiI0OWE2OGQyMS03NTE1LTRmNDMtODZhZC1hMDNhMWNmNDFmNTgifQ.EaWmmuDhoa8jt_go4zrY5AhTGnljIS1zNwIF-f9eBidAiWPYaKwmXRsRoXrAeybLcJ5E8-7TO1S8jZiCxcElQPdRRvuP9ZsgNhfEqDhZtabkwGBFt4gpQZnFsgGDMAi-v4sTM55VsnIJIMrHPNIckZfL-YhD-FSCtDsqrCkCnucXR58Kfp_SEx3hJvrU-2vcOeNXJBTqs3pHl9jws9GJIf6bZd7vQqTMolCC4zBzUp7yT3OXqMt9uCUhLZzLtpTRW0_-u-KW4h44eHOdEf6ePqiFhjflF9_T6cM-OBqzaK0eVvg6RcasvF3IyxdG-ME5crapAibeePqg_Hy__Q5idQ' + self.session = OidcSession.objects.create( + user_uid="", + nonce="", + authz_request={"scope": "offline_access", "prompt": "consent", "nonce": "123", "acr_values":["https://www.spid.gov.it/SpidL1"]}, + client_id="", + auth_code="code", + ) + + def test_validate_authz_request_object(self): + with self.assertRaises(Exception): + OpBase.validate_authz_request_object(self,{}) + + def test_check_client_assertion(self): + with self.assertRaises(Exception): + OpBase.check_client_assertion(self, client_id = "", client_assertion = self.client_assertion) + + def test_get_refresh_token(self): + result = OpBase.get_refresh_token( + self, + iss_sub = "sub", + sub = "sub", + authz = self.session, + jwt_at = "jwt_at", + commons= {} + ) + self.assertEqual(result["sub"], "sub") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_request_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_request_settings.py new file mode 100644 index 0000000..6a6e9ae --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_request_settings.py @@ -0,0 +1,107 @@ +from copy import deepcopy + +from spid_cie_oidc.entity.utils import exp_from_now, iat_now + +TOKEN_REQUEST = { + "client_id": "https://rp.cie.it", + "client_assertion": "head.payload.verifysignature", + "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", +} + +TOKEN_AUTHN_CODE_REQUEST = deepcopy(TOKEN_REQUEST) +TOKEN_AUTHN_CODE_REQUEST["code"] = "usDwMnEzJPpG5oaV8x3j&" +TOKEN_AUTHN_CODE_REQUEST["code_verifier"] = "9g8S40MozM3NSqjHnhi7OnsE38jklFv2&" +TOKEN_AUTHN_CODE_REQUEST["grant_type"] = "authorization_code" + +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ID.pop("client_id") + +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION.pop("client_assertion") + +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION = deepcopy( + TOKEN_AUTHN_CODE_REQUEST +) +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION["client_assertion"] = "head..sign" + +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CLIENT_ASSERTION_TYPE.pop("client_assertion_type") + +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE = deepcopy( + TOKEN_AUTHN_CODE_REQUEST +) +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_CLIENT_ASSERTION_TYPE[ + "client_assertion_type" +] = ":params:oauth:client-assertion-type:" + +TOKEN_AUTHN_CODE_REQUEST_NO_CODE = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CODE.pop("code") + +TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CODE_VERIFY.pop("code_verifier") + +TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_GRANT_TYPE.pop("grant_type") + +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE = deepcopy(TOKEN_AUTHN_CODE_REQUEST) +TOKEN_AUTHN_CODE_REQUEST_NO_CORRECT_GRANT_TYPE["grant_type"] = "refresh_token" + +TOKEN_REFRESH_REQUEST = deepcopy(TOKEN_REQUEST) +TOKEN_REFRESH_REQUEST["grant_type"] = "refresh_token" +TOKEN_REFRESH_REQUEST["refresh_token"] = "8xLOxBtZp8" + +TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE = deepcopy(TOKEN_REFRESH_REQUEST) +TOKEN_REFRESH_REQUEST_NO_GRANT_TYPE.pop("grant_type") + +TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE = deepcopy(TOKEN_REFRESH_REQUEST) +TOKEN_REFRESH_REQUEST_NO_CORRECT_GRANT_TYPE["grant_type"] = "authorization_code" + +# TODO: we need factory function to get fresh now +IAT = iat_now() +EXP = exp_from_now() + +JWT_CLIENT_ASSERTION = { + "iss": "https://rp.cie.it", + "sub": "https://rp.cie.it", + "iat": IAT, + "exp": EXP, + "aud": ["https://rp.spid.agid.gov.it/auth"], + "jti": "a72d5df0-2415-4c7c-a44f-3988b354040b", +} + + +JWT_CLIENT_ASSERTION_NO_ISS = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_ISS.pop("iss") + +JWT_CLIENT_ASSERTION_NO_CORRECT_ISS = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_ISS["iss"] = "https://idserve.i" + +JWT_CLIENT_ASSERTION_NO_SUB = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_SUB.pop("sub") + +JWT_CLIENT_ASSERTION_NO_CORRECT_SUB = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_SUB["sub"] = "https://idserver" + +JWT_CLIENT_ASSERTION_NO_IAT = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_IAT.pop("iat") + +JWT_CLIENT_ASSERTION_NO_CORRECT_IAT = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_IAT["iat"] = str(IAT + 400) + +JWT_CLIENT_ASSERTION_NO_EXP = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_EXP.pop("exp") + +JWT_CLIENT_ASSERTION_NO_CORRECT_EXP = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_EXP["exp"] = 1000 + +JWT_CLIENT_ASSERTION_NO_JTI = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_JTI.pop("jti") + +JWT_CLIENT_ASSERTION_NO_CORRECT_JTI = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_JTI["jti"] = [] + +JWT_CLIENT_ASSERTION_NO_AUD = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_AUD.pop("aud") + +JWT_CLIENT_ASSERTION_NO_CORRECT_AUD = deepcopy(JWT_CLIENT_ASSERTION) +JWT_CLIENT_ASSERTION_NO_CORRECT_AUD["aud"] = "https://rp.spid.agid.gov.it/auth" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_response_settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_response_settings.py new file mode 100644 index 0000000..d545f31 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/tests/token_response_settings.py @@ -0,0 +1,47 @@ +from copy import deepcopy + + +TOKEN_RESPONSE = { + "access_token": "dC34Pf6kdG.Gjpw5HN6c.JSKABYDGye6", + "token_type": "Bearer", + "expires_in": 1800, + "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY.ashajrbGFDGd", +} + +TOKEN_RESPONSE_NO_ACCESS_TOKEN = deepcopy(TOKEN_RESPONSE) +TOKEN_RESPONSE_NO_ACCESS_TOKEN.pop("access_token") + +TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN = deepcopy(TOKEN_RESPONSE) +TOKEN_RESPONSE_NO_CORRECT_ACCESS_TOKEN["access_token"] = "dC34Pf6kdG.Gjpw5HN" + +TOKEN_RESPONSE_NO_TOKEN_TYPE = deepcopy(TOKEN_RESPONSE) +TOKEN_RESPONSE_NO_TOKEN_TYPE.pop("token_type") + +TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE = deepcopy(TOKEN_RESPONSE) +TOKEN_RESPONSE_NO_CORRECT_TOKEN_TYPE["token_type"] = "prova" + +TOKEN_REFRESH_RESPONSE = deepcopy(TOKEN_RESPONSE) +TOKEN_REFRESH_RESPONSE["refresh_token"] = "wJ848BcyLP.hHhJB.kjHMjUGUSGKEE3" + +TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN = deepcopy(TOKEN_REFRESH_RESPONSE) +TOKEN_REFRESH_RESPONSE_NO_REFRESH_TOKEN.pop("refresh_token") + +TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN = deepcopy(TOKEN_REFRESH_RESPONSE) +TOKEN_REFRESH_RESPONSE_NO_CORRECT_REFRESH_TOKEN["refresh_token"] = "dC34Pf6kdG..Gjpw5HN" + +TOKEN_ERROR_RESPONSE = { + "error": "invalid_request", + "error_description": "descrizione dell’errore", +} + +TOKEN_ERROR_RESPONSE_NO_ERROR = deepcopy(TOKEN_ERROR_RESPONSE) +TOKEN_ERROR_RESPONSE_NO_ERROR.pop("error") + +TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR = deepcopy(TOKEN_ERROR_RESPONSE) +TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR["error"] = "" + +TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION = deepcopy(TOKEN_ERROR_RESPONSE) +TOKEN_ERROR_RESPONSE_NO_ERROR_DESCRIPTION.pop("error_description") + +TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION = deepcopy(TOKEN_ERROR_RESPONSE) +TOKEN_ERROR_RESPONSE_NO_CORRECT_ERROR_DESCRIPTION["error"] = [] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/urls.py new file mode 100644 index 0000000..db91832 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/urls.py @@ -0,0 +1,84 @@ +from django.conf import settings +from django.urls import path + +from .views.authz_request_view import AuthzRequestView +from .views.userinfo_endpoint import UserInfoEndpoint +from spid_cie_oidc.entity.views import ( + openid_connect_jwks_uri, + openid_connect_signed_jwks_uri +) +from .views.connect import openid_configuration +from .views.consent_page_view import ( + ConsentPageView, + oidc_provider_not_consent, + UserAccessHistoryView, + RevokeSessionView, +) +from .views.token_endpoint import TokenEndpoint +from .views.revocation_endpoint import RevocationEndpoint +from .views.introspection_endpoint import IntrospectionEndpoint + +_PREF = getattr(settings, "OIDC_PREFIX", "oidc/op") + +urlpatterns = [ + path( + f"{_PREF}/authorization", + AuthzRequestView.as_view(), + name="oidc_provider_authnrequest", + ), + path( + f"{_PREF}/consent", + ConsentPageView.as_view(), + name="oidc_provider_consent", + ), + path( + f"{_PREF}/token", + TokenEndpoint.as_view(), + name="oidc_provider_token_endpoint", + ), + path( + f"{_PREF}/userinfo", + UserInfoEndpoint.as_view(), + name="oidc_provider_userinfo_endpoint", + ), + path( + f"{_PREF}/revocation", + RevocationEndpoint.as_view(), + name="oidc_provider_end_session_endpoint", + ), + path( + f"{_PREF}/introspection", + IntrospectionEndpoint.as_view(), + name="oidc_provider_introspection_endpoint", + ), + path( + f"{_PREF}/notconsent", + oidc_provider_not_consent, + name="oidc_provider_not_consent", + ), + path( + f"{_PREF}/history", + UserAccessHistoryView.as_view(), + name="oidc_provider_access_history", + ), + path( + f"{_PREF}/revoke", + RevokeSessionView.as_view(), + name="oidc_provider_revoke_session", + ), + path( + f"{_PREF}/.well-known/openid-configuration", + openid_configuration, + name="oidc_provider_openid_configuration", + ), + path( + f"{_PREF}//jwks.json", + openid_connect_jwks_uri, + name="oidc_connect_jwks_uri", + ), + path( + f"{_PREF}//jwks.jose", + openid_connect_signed_jwks_uri, + name="oidc_connect_signed_jwks_uri", + ), +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/__init__.py new file mode 100644 index 0000000..d154e66 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/__init__.py @@ -0,0 +1,414 @@ +import logging +import uuid +from cryptojwt.jws.utils import left_hash +from django.conf import settings +from pydantic import ValidationError +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.utils import timezone +import urllib +from spid_cie_oidc.entity.jwtse import create_jws, unpad_jwt_head, unpad_jwt_payload, verify_jws +from spid_cie_oidc.entity.models import FederationEntityConfiguration, TrustChain +from spid_cie_oidc.entity.settings import HTTPC_PARAMS +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain +from spid_cie_oidc.entity.utils import datetime_from_timestamp, exp_from_now, iat_now +from spid_cie_oidc.entity.utils import get_jwks +from spid_cie_oidc.entity.exceptions import TrustchainMissingMetadata +from spid_cie_oidc.provider.exceptions import ( + AuthzRequestReplay, + ExpiredAuthCode, + InvalidSession, + RevokedSession, + ValidationException +) +from spid_cie_oidc.provider.models import OidcSession +from spid_cie_oidc.provider.schemas.client_assertion import ClientAssertion + +from spid_cie_oidc.provider.settings import ( + OIDCFED_ATTRNAME_I18N, + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE, + OIDCFED_PROVIDER_PROFILES, + OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH, + OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS +) + +logger = logging.getLogger(__name__) + + +class OpBase: + """ + Baseclass with common methods for OPs + """ + + def redirect_response_data(self, redirect_uri: str, **kwargs) -> HttpResponseRedirect: + if "?" in redirect_uri: + qstring = "&" + else: + qstring = "?" + url = f'{redirect_uri}{qstring}{urllib.parse.urlencode(kwargs)}' + return HttpResponseRedirect(url) + + def find_jwk(self, header: dict, jwks: list) -> dict: + for jwk in jwks: + valid_jwk = jwk.get("kid", None) + if valid_jwk and header["kid"] == valid_jwk: + return jwk + + def validate_authz_request_object(self, req) -> TrustChain: + state = getattr(self, 'payload', {}).get("state", "") + try: + self.payload = unpad_jwt_payload(req) + header = unpad_jwt_head(req) + except Exception as e: + _msg = ( + f"Error in Authz request object {dict(req.GET)}: {e}." + f" error=invalid_request" + f"state={state}" + ) + logger.error(_msg) + raise Exception(_msg) + + self.is_a_replay_authz() + rp_trust_chain = TrustChain.objects.filter( + metadata__openid_relying_party__isnull=False, + sub=self.payload["iss"], + trust_anchor__sub__in=settings.OIDCFED_TRUST_ANCHORS + ).first() + if rp_trust_chain and not rp_trust_chain.is_active: + _msg = ( + f"Disabled client {rp_trust_chain.sub} requests an authorization. " + "error = access_denied, " + f"state={state}" + ) + logger.warning(_msg) + raise Exception(_msg) + + elif not rp_trust_chain or rp_trust_chain.is_expired: + rp_trust_chain = None + # TODO: get async here + for ta in settings.OIDCFED_TRUST_ANCHORS: + try: + rp_trust_chain = get_or_create_trust_chain( + subject=self.payload["iss"], + trust_anchor=ta, + httpc_params=HTTPC_PARAMS, + required_trust_marks=getattr( + settings, "OIDCFED_REQUIRED_TRUST_MARKS", [] + ), + ) + if rp_trust_chain and rp_trust_chain.metadata: + break + except TrustchainMissingMetadata as e: + logger.debug(f"TrustchainMissingMetadata: {e}") + # unless we find the good TA + continue + + if not rp_trust_chain or not rp_trust_chain.is_valid: + _msg = ( + f"Failed trust chain validation for {self.payload['iss']}. " + "error=unauthorized_client, " + f"state={state}" + ) + logger.warning(_msg) + raise Exception(_msg) + + jwks = get_jwks( + rp_trust_chain.metadata['openid_relying_party'], + federation_jwks=rp_trust_chain.jwks + ) + jwk = self.find_jwk(header, jwks) + if not jwk: + _msg = ( + f"Invalid jwk for {self.payload['iss']}. " + f"{header['kid']} not found in {jwks}. " + "error=unauthorized_client, " + f"state={state}" + ) + logger.error(_msg) + raise Exception(_msg) + + try: + verify_jws(req, jwk) + except Exception as e: + _msg = ( + "Authz request object signature validation failed " + f"for {self.payload['iss']}: {e}. " + f"error=access_denied, state={state}" + ) + logger.error(_msg) + raise Exception(_msg) + + return rp_trust_chain + + def is_a_replay_authz(self): + preexistent_authz = OidcSession.objects.filter( + client_id=self.payload["client_id"], + nonce=self.payload["nonce"] + ).first() + if preexistent_authz: + raise AuthzRequestReplay( + f"{preexistent_authz.client_id} with {preexistent_authz.nonce}" + ) + + def check_session(self, request) -> OidcSession: + if not request.user.is_authenticated: + raise InvalidSession() + + auth_code = request.session.get("oidc", {}).get("auth_code", None) + if not auth_code: + # FIXME: to do test + raise InvalidSession() + + session = OidcSession.objects.filter( + auth_code=request.session["oidc"]["auth_code"] + ).first() + + if not session: + raise InvalidSession("Unknown Session") + + if session.user != request.user: + raise InvalidSession( + f"Different user, session is of {session.user} " + f"but request user is {request.user}" + ) + + if session.revoked: + raise RevokedSession( + "Session was revoked" + ) + + session_not_after = session.created + timezone.timedelta( + minutes=OIDCFED_PROVIDER_AUTH_CODE_MAX_AGE + ) + if session_not_after < timezone.localtime(): + raise ExpiredAuthCode( + f"The Session is expired at {session_not_after}" + ) + return session + + def get_issuer(self): + return FederationEntityConfiguration.objects.filter( + entity_type="openid_provider" + ).first() + + def check_client_assertion(self, client_id: str, client_assertion: str) -> bool: + head = unpad_jwt_head(client_assertion) + payload = unpad_jwt_payload(client_assertion) + _sub = payload.get('sub', None) + _aud = payload.get('aud', []) + _op = self.get_issuer() + _op_eid = _op.sub + _op_eid_authz_endpoint = [_op.metadata['openid_provider']['authorization_endpoint']] + + try: + ClientAssertion(**payload) + except Exception as e: + raise Exception(f"Client Assertion: json schema validation error: {e}") + + if isinstance(_aud, str): + _aud = [_aud] + _allowed_auds = _aud + _op_eid_authz_endpoint + + if _sub != client_id: + logger.warning( + f"Client assertion failed: {_sub} != {client_id}" + ) + # TODO Specialize exceptions + raise Exception("Client Assertion: sub != client_id") + + if _op_eid: + _allowed_auds.append(_op_eid) + + if not _op_eid or self.request.build_absolute_uri() not in _allowed_auds: + logger.warning( + "Client assertion failed, fake audience: " + f"{self.request.build_absolute_uri()} not in {_allowed_auds}" + ) + # TODO Specialize exceptions + raise Exception("Client Assertion: fake audience") + + tc = TrustChain.objects.get(sub=client_id, is_active=True) + jwk = self.find_jwk(head, tc.metadata['openid_relying_party']['jwks']['keys']) + verify_jws(client_assertion, jwk) + return True + + def validate_json_schema(self, payload, schema_type, error_description): + try: + schema = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + schema[schema_type](**payload) + except (ValidationError, Exception) as e: + logger.error( + f"{error_description} " + f"for {payload.get('client_id', None)}: {e}" + ) + raise ValidationException() + + def get_jwt_common_data(self): + return { + "exp": exp_from_now(), + "iat": iat_now() + } + + def get_access_token( + self, iss_sub: str, sub: str, authz: OidcSession, commons: dict + ) -> dict: + + access_token = { + "iss": iss_sub, + "sub": sub, + "aud": [iss_sub, reverse("oidc_provider_userinfo_endpoint")], + "client_id": authz.client_id, + "scope": authz.authz_request["scope"], + "jti": str(uuid.uuid4()) + } + access_token.update(commons) + + return access_token + + def get_id_token_claims( + self, + authz: OidcSession + ) -> dict: + _provider_profile = getattr(settings, 'OIDCFED_DEFAULT_PROVIDER_PROFILE', OIDCFED_DEFAULT_PROVIDER_PROFILE) + claims = {} + allowed_id_token_claims = OIDCFED_PROVIDER_PROFILES_ID_TOKEN_CLAIMS[_provider_profile] + if not allowed_id_token_claims: + return claims + + for claim in ( + authz.authz_request.get( + "claims", {} + ).get("id_token", {}).keys() + ): + if claim in allowed_id_token_claims and authz.user.attributes.get(claim, None): + claims[claim] = authz.user.attributes[claim] + return claims + + def get_id_token( + self, + iss_sub: str, + sub: str, + authz: OidcSession, + jwt_at: str, + commons: dict + ) -> dict: + + id_token = { + "sub": sub, + "nonce": authz.authz_request["nonce"], + "at_hash": left_hash(jwt_at, "HS256"), + "c_hash": left_hash(authz.auth_code, "HS256"), + "aud": [authz.client_id], + "iss": iss_sub, + "acr": authz.acr, + "jti": str(uuid.uuid4()) + } + claims = self.get_id_token_claims(authz) + if claims: + id_token.update(claims) + + id_token.update(commons) + return id_token + + def get_refresh_token( + self, + iss_sub: str, + sub: str, + authz: OidcSession, + jwt_at: str, + commons: dict + ) -> dict: + # refresh token is scope offline_access and prompt == consent + refresh_acrs = OIDCFED_PROVIDER_PROFILES_ACR_4_REFRESH[OIDCFED_DEFAULT_PROVIDER_PROFILE] + acrs = authz.authz_request.get('acr_values', []) + if ( + "offline_access" in authz.authz_request['scope'] and + 'consent' in authz.authz_request['prompt'] and + set(refresh_acrs).intersection(set(acrs)) + ): + refresh_token = { + "sub": sub, + "at_hash": left_hash(jwt_at, "HS256"), + "c_hash": left_hash(authz.auth_code, "HS256"), + "aud": authz.client_id, + "iss": iss_sub, + "jti": str(uuid.uuid4()) + } + refresh_token.update(commons) + return refresh_token + + def get_iss_token_data(self, session: OidcSession, issuer: FederationEntityConfiguration): + _sub = session.pairwised_sub(provider_id=issuer.sub) + iss_sub = issuer.sub + commons = self.get_jwt_common_data() + jwk = issuer.jwks_core[0] + + access_token = self.get_access_token(iss_sub, _sub, session, commons) + jwt_at = create_jws(access_token, jwk, typ="at+jwt") + id_token = self.get_id_token(iss_sub, _sub, session, jwt_at, commons) + jwt_id = create_jws(id_token, jwk) + + iss_token_data = dict( + session=session, + access_token=jwt_at, + id_token=jwt_id, + expires=datetime_from_timestamp(commons["exp"]) + ) + + _refresh_token = self.get_refresh_token(iss_sub, _sub, session, jwt_at, commons) + if _refresh_token: + jwt_rt = create_jws(_refresh_token, jwk) + iss_token_data["refresh_token"] = jwt_rt + return iss_token_data + + def get_expires_in(self, iat: int, exp: int): + return timezone.timedelta( + seconds=exp - iat + ).seconds + + def attributes_names_to_release(self, request, session: OidcSession) -> dict: + # get up fresh claims + user_claims = request.user.attributes + user_claims["email"] = user_claims.get("email", request.user.email) + user_claims["username"] = request.user.username + + # filter on requested claims + filtered_user_claims = {} + for target, claims in session.authz_request.get("claims", {}).items(): + for claim in claims: + if claim in user_claims: + filtered_user_claims[claim] = user_claims[claim] + # IDA support/overload of the claims, the verified has priorities and overwrite the unverified + if "verified_claims" in claims: + claims = claims["verified_claims"].get('claims', {}) + for claim in claims: + if claim in user_claims: + filtered_user_claims[claim] = user_claims[claim] + + # mapping with human names + i18n_user_claims = [ + OIDCFED_ATTRNAME_I18N.get(i, i) + for i in filtered_user_claims.keys() + ] + return dict( + i18n_user_claims=i18n_user_claims, + filtered_user_claims=filtered_user_claims + ) + + def get_client_organization_name(self, tc): + rp_metadata = ( + tc.metadata.get( + "federation_entity", {} + ) or + tc.metadata.get( + "openid_relying_party", {} + ) + ) + if rp_metadata: + name = ( + rp_metadata.get("organization_name", "") or + rp_metadata.get("client_name", "") or + rp_metadata.get("client_id", "") + ) + return name diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/authz_request_view.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/authz_request_view.py new file mode 100644 index 0000000..63d6ace --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/authz_request_view.py @@ -0,0 +1,303 @@ +import hashlib +import logging +import urllib.parse +import uuid +import json + +from djagger.decorators import schema +from django.conf import settings +from django.contrib.auth import authenticate, login, logout +from django.forms.utils import ErrorList +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + HttpResponseRedirect +) +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import gettext as _ +from django.views import View +from spid_cie_oidc.entity.exceptions import InvalidEntityConfiguration +from spid_cie_oidc.provider.schemas.authn_requests import AcrValues +from spid_cie_oidc.provider.forms import AuthLoginForm, AuthzHiddenForm +from spid_cie_oidc.provider.models import OidcSession +from spid_cie_oidc.provider.exceptions import AuthzRequestReplay, InvalidRefreshRequestException, ValidationException +from spid_cie_oidc.provider.settings import ( + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_PROFILES, + OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR +) +from . import OpBase +logger = logging.getLogger(__name__) + + +schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + + +@schema( + summary="OIDC Provider Authorization endpoint", + methods=['GET', 'POST'], + get_request_schema = { + "application/x-www-form-urlencoded": schema_profile["authorization_request_doc"], + "request object - jwt payload": schema_profile["authorization_request"] + }, + post_response_schema= { + "302":schema_profile["authorization_response"], + "403": schema_profile["authorization_error_response"] + }, + external_docs = { + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags = ['Provider'] +) +class AuthzRequestView(OpBase, View): + """ + View which processes the actual Authz request and + returns a Http Redirect + """ + + template = "op_user_login.html" + + def string_to_list(self, payload, must_list): + for i in must_list: + if isinstance(payload.get(i, None), str): + if ' ' in payload[i]: + payload[i] = payload[i].split(' ') + else: + payload[i] = [payload[i]] + return payload + + def validate_authz(self, payload: dict): + + must_list = ("scope", "acr_values") + payload = self.string_to_list(payload, must_list) + if ( + 'offline_access' in payload['scope'] and + 'consent' not in payload['prompt'] + ): + raise InvalidRefreshRequestException( + "scope with offline_access without prompt = consent" + ) + redirect_uri = payload.get("redirect_uri", "") + p = urllib.parse.urlparse(redirect_uri) + scheme_fqdn = f"{p.scheme}://{p.hostname}" + client_id = payload.get("client_id") or "" + if scheme_fqdn not in client_id: + raise ValidationException("client_id not in redirect_uri") + + self.validate_json_schema( + payload, + "authorization_request", + "Authen request object validation failed " + ) + + def get_url_consent(self, user): + url = reverse("oidc_provider_consent") + if ( + user.is_staff and + 'spid_cie_oidc.relying_party_test' in settings.INSTALLED_APPS + ): + try: + url = reverse("oidc_provider_staff_testing") + except Exception as e: # pragma: no cover + logger.error(f"testigng page url reverse failed: {e}") + return url + + def get_login_form(self): + return AuthLoginForm + + def get(self, request, *args, **kwargs): + """ + The Authorization request of a RPs is validated and a login prompt is rendered to the user + """ + req = request.GET.get("request", None) + if not req: + logger.error( + f"Missing Authz request object in {dict(request.GET)} " + f"error=invalid_request" + ) + return HttpResponseBadRequest() + # yes, again. We MUST. + tc = None + try: + tc = self.validate_authz_request_object(req) + except InvalidEntityConfiguration as e: + # FIXME: to do test + logger.error(f"Invalid Entity Configuration: {e}") + return self.redirect_response_data( + self.payload["redirect_uri"], + error = "invalid_request", + error_description =_("Failed to establish the Trust"), + state = self.payload.get("state", "") + ) + except AuthzRequestReplay as e: + logger.error( + "Replay on authz request detected for " + f"{request.GET.get('client_id', 'unknow')}: {e}" + ) + return self.redirect_response_data( + self.payload["redirect_uri"], + error = "invalid_request", + error_description =_( + "An Unknown error raised during validation of " + f" authz request object: {e}" + ), + state = self.payload.get("state", "") + + ) + except Exception as e: + logger.error( + "Error during authz request validation for " + f"{request.GET.get('client_id', 'unknown')}: {e}" + ) + return self.redirect_response_data( + self.payload["redirect_uri"], + error="invalid_request", + error_description=_("Authorization request not valid"), + state = self.payload.get("state", "") + + ) + try: + self.validate_authz(self.payload) + except ValidationException: + return self.redirect_response_data( + self.payload["redirect_uri"], + error="invalid_request", + error_description=_("Authorization request validation error"), + state = self.payload.get("state", "") + ) + except InvalidRefreshRequestException as e: + logger.warning(f"Invalid session: {e}") + return HttpResponseForbidden() + if self.payload.get("acr_values", None): + acr_value = AcrValues(self.payload["acr_values"][0]) + else: + # set this as default + acr_value = AcrValues.l2 + prompt = self.payload.get("prompt", "login") + if request.user: + if ( + request.user.is_authenticated and + acr_value == AcrValues.l1 and + "login" not in prompt + ): + try: + session = self.check_session(request) + if session.acr != AcrValues.l1.value: + logout(request) + return self.get(request) + else: + url = self.get_url_consent(request.user) + return HttpResponseRedirect(url) + except Exception: + logger.warning( + f"Failed SSO check session for {request.user}" + ) + logout(request) + return self.get(request) + + # stores the authz request in a hidden field in the form + form = self.get_login_form()() + context = { + "client_organization_name": self.get_client_organization_name(tc), + "hidden_form": AuthzHiddenForm(dict(authz_request_object=req)), + "form": form, + "redirect_uri": self.payload["redirect_uri"], + "obj_request": json.dumps(self.payload, indent=2), + "acr_value": acr_value.name, + "state": self.payload["state"] + } + return render(request, self.template, context) + + def post(self, request, *args, **kwargs): + """ + When the User prompts his credentials + TODO: REFACTOR this method doesn't support PAR! + """ + form = self.get_login_form()(request.POST) + if not form.is_valid(): + return render( + request, + self.template, + { + "form": form, + "hidden_form": AuthzHiddenForm(request.POST), + } + ) + + authz_form = AuthzHiddenForm(request.POST) + authz_form.is_valid() + authz_request = authz_form.cleaned_data.get("authz_request_object") + try: + self.validate_authz_request_object(authz_request) + except Exception as e: + logger.error( + "Authz request object validation failed " + f"for {authz_request}: {e} " + ) + # we don't have a redirect_uri here + return HttpResponseForbidden() + + # autenticate the user + username = form.cleaned_data.get("username") + password = form.cleaned_data.get("password") + user = authenticate(username=username, password=password) + + if not user: + errors = form._errors.setdefault("username", ErrorList()) + errors.append(_("invalid username or password")) + return render( + request, + self.template, + { + "form": form, + "hidden_form": AuthzHiddenForm(request.POST), + "redirect_uri": self.payload["redirect_uri"], + "state": self.payload["state"] + } + ) + else: + login(request, user) + + # create auth_code + auth_code = hashlib.sha512( + '-'.join( + ( + f'{uuid.uuid4()}', + f'{self.payload["client_id"]}', + f'{self.payload["nonce"]}' + ) + ).encode() + ).hexdigest() + # put the auth_code in the user web session + request.session["oidc"] = {"auth_code": auth_code} + + # store the User session + _provider_profile = getattr( + settings, + 'OIDCFED_DEFAULT_PROVIDER_PROFILE', + OIDCFED_DEFAULT_PROVIDER_PROFILE + ) + default_acr = OIDCFED_PROVIDER_PROFILES_DEFAULT_ACR[_provider_profile] + self.payload = self.string_to_list(self.payload, ["acr_values"]) + len_acr = len(self.payload.get("acr_values",[])) + session = OidcSession.objects.create( + user=user, + user_uid=user.username, + nonce=self.payload["nonce"], + authz_request=self.payload, + client_id=self.payload["client_id"], + auth_code=auth_code, + acr=( + self.payload["acr_values"][len_acr - 1] + if len_acr > 0 + else default_acr + ) + ) + session.set_sid(request) + url = self.get_url_consent(user) + return HttpResponseRedirect(url) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/connect.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/connect.py new file mode 100644 index 0000000..f484f7a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/connect.py @@ -0,0 +1,34 @@ +import logging + +from djagger.decorators import schema +from django.http import Http404 +from django.http import JsonResponse + +from spid_cie_oidc.entity.models import FederationEntityConfiguration +logger = logging.getLogger(__name__) + + +@schema( + summary="OIDC Provider openid-configuration", + methods=['GET'], + tags = ['Provider'] +) +def openid_configuration(request): + """ + OIDC Discovery configuration at + .well-known/openid-configuration + """ + _sub = request.build_absolute_uri().split("/.well-known/openid-configuration")[0] + conf = FederationEntityConfiguration.objects.filter( + # TODO: check for reverse proxy and forwarders ... + sub=_sub, + is_active=True, + ).first() + if not conf: # pragma: no cover + raise Http404() + + content = conf.entity_configuration_as_dict['metadata'].get('openid_provider', None) + if content: + return JsonResponse(content, safe=False) + else: + raise Http404() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/consent_page_view.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/consent_page_view.py new file mode 100644 index 0000000..ceae5c8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/consent_page_view.py @@ -0,0 +1,157 @@ +import logging +from django.core.paginator import Paginator +import urllib.parse +from urllib.parse import urlparse + +from djagger.decorators import schema +from django.contrib.auth import logout +from django.http import ( + HttpResponseForbidden, + HttpResponseRedirect, +) +from django.shortcuts import redirect, render +from django.utils.translation import gettext as _ +from django.views import View +from spid_cie_oidc.entity.models import ( + TrustChain +) +from spid_cie_oidc.provider.forms import ConsentPageForm +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.settings import OIDCFED_PROVIDER_HISTORY_PER_PAGE + +from . import OpBase + +logger = logging.getLogger(__name__) + + +class ConsentPageView(OpBase, View): + + template = "op_user_consent.html" + + def get_consent_form(self): + return ConsentPageForm + + def get(self, request, *args, **kwargs): + try: + session = self.check_session(request) + except Exception: + logger.warning("Invalid session on Consent page") + return HttpResponseForbidden() + + tc = TrustChain.objects.filter( + sub=session.client_id, + metadata__openid_relying_party__isnull=False, + is_active = True + ).first() + + # if this auth code was already been used ... forbidden + if IssuedToken.objects.filter(session=session): + logger.warning(f"Auth code Replay {session}") + return HttpResponseForbidden() + + i18n_user_claims = self.attributes_names_to_release( + request, session + )['i18n_user_claims'] + + context = { + "form": self.get_consent_form()(), + "session": session, + "client_organization_name": self.get_client_organization_name(tc), + "user_claims": sorted(set(i18n_user_claims),), + "redirect_uri": session.authz_request["redirect_uri"], + "state": session.authz_request["state"] + } + return render(request, self.template, context) + + def post(self, request, *args, **kwargs): + try: + session = self.check_session(request) + except Exception as e: + logger.warning(f"Invalid session: {e}") + return HttpResponseForbidden() + + self.payload = session.authz_request + form = self.get_consent_form()(request.POST) + if not form.is_valid(): + return self.redirect_response_data( + self.payload["redirect_uri"], + # TODO: this is not normative -> check AgID/IPZS + error="rejected_by_user", + error_description=_("User rejected the release of attributes"), + state = self.payload.get("state", "") + ) + issuer = self.get_issuer() + + iss_token_data = self.get_iss_token_data(session, issuer) + IssuedToken.objects.create(**iss_token_data) + + return self.redirect_response_data( + self.payload["redirect_uri"], + code=session.auth_code, + state=session.authz_request["state"], + iss=issuer.sub if issuer else "", + ) + + +def oidc_provider_not_consent(request): + redirect_uri = urlparse(request.GET.get("redirect_uri")) + state = request.GET.get("state", "") + logout(request) + kwargs = dict( + error = "invalid_request", + error_description = _( + "Authentication request rejected by user" + ), + state = state + ) + path = redirect_uri.path if redirect_uri.path else "/" + base = f'{redirect_uri.scheme}://{redirect_uri.netloc}{path}' + url = f'{base}?{urllib.parse.urlencode(kwargs)}' + return HttpResponseRedirect(url) + + +class UserAccessHistoryView(OpBase, View): + + def get(self, request, *args, **kwargs): + try: + session = self.check_session(request) + except Exception: # pragma: no cover + logger.warning("Invalid session on Access History page") + return HttpResponseForbidden() + user_access_history = OidcSession.objects.filter( + user_uid=session.user_uid, + ).exclude(auth_code=session.auth_code) + paginator = Paginator( + user_access_history, + OIDCFED_PROVIDER_HISTORY_PER_PAGE + ) + page = request.GET.get("page", 1) + history = paginator.get_page(page) + context = { + "history": history, + "user": request.user + } + return render(request, "op_user_history.html", context) + + +@schema( + methods = [], + djagger_exclude=True +) +class RevokeSessionView(OpBase, View): + + def get(self, request, *args, **kwargs): + try: + self.check_session(request) + except Exception: # pragma: no cover + logger.warning("Invalid session on revoke page") + return HttpResponseForbidden() + + if request.GET.get("auth_code"): + auth_code = request.GET["auth_code"] + session_to_revoke = OidcSession.objects.filter( + auth_code=auth_code, + user=request.user + ).first() + session_to_revoke.revoke(destroy_session=False) + return redirect("oidc_provider_access_history") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/introspection_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/introspection_endpoint.py new file mode 100644 index 0000000..e6a7145 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/introspection_endpoint.py @@ -0,0 +1,97 @@ +import logging + +from djagger.decorators import schema +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + JsonResponse +) +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from pydantic import BaseModel +from spid_cie_oidc.provider.exceptions import ValidationException +from spid_cie_oidc.provider.models import IssuedToken +from spid_cie_oidc.provider.settings import ( + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_PROFILES +) + +from . import OpBase +logger = logging.getLogger(__name__) + + +schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + + +@schema( + methods=['GET', 'POST'], + post_request_schema = { + "application/x-www-form-urlencoded": schema_profile["introspection_request"] + }, + post_response_schema= { + "200":schema_profile["introspection_response"], + "400":schema_profile["introspection_error_response"], + }, + get_response_schema= { + "400": BaseModel + }, + tags = ['Provider'] +) +@method_decorator(csrf_exempt, name="dispatch") +class IntrospectionEndpoint(OpBase, View): + def get(self, request, *args, **kwargs): + return HttpResponseBadRequest() + + def post(self, request, *args, **kwargs): + try: + self.validate_json_schema( + request.POST.dict(), + "introspection_request", + "Introspection request object validation failed" + ) + client_id = request.POST['client_id'] + self.check_client_assertion( + client_id, + request.POST['client_assertion'] + ) + except ValidationException: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Introspection request object validation failed", + }, + status = 400 + ) + except Exception as e: # pragma: no cover + logger.error(e) + return HttpResponseForbidden() + + required_token = request.POST['token'] + # query con client_id, access token + token = IssuedToken.objects.filter( + access_token=required_token + ).first() + session = token.session + if session.client_id != client_id: + return JsonResponse( # pragma: no cover + error = "invalid_client", + error_description = "Client not recognized" + ) + active = token and not token.is_revoked and not token.expired + exp = token.expires + sub = token.session.client_id + issuer = self.get_issuer() + iss = issuer.sub + authz_request = session.authz_request + scope = authz_request["scope"] + response = { + "active": active, + "exp": exp, + "sub" : sub, + "iss": iss, + "client_id": client_id, + "aud": [client_id], + "scope": scope + } + return JsonResponse(response) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/revocation_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/revocation_endpoint.py new file mode 100644 index 0000000..8550a48 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/revocation_endpoint.py @@ -0,0 +1,105 @@ +import logging + +from djagger.decorators import schema +from django.http import ( + HttpResponse, + JsonResponse +) +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from spid_cie_oidc.provider.exceptions import ValidationException +from spid_cie_oidc.provider.models import IssuedToken +from spid_cie_oidc.provider.settings import ( + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_PROFILES +) + +from . import OpBase +logger = logging.getLogger(__name__) + + +schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + + +@schema( + methods=['POST'], + post_request_schema = { + "application/x-www-form-urlencoded": schema_profile["revocation_request"], + "application/json": schema_profile["revocation_request"], + }, + post_response_schema = { + "200": {}, + "400": schema_profile["revocation_response"] + }, + tags = ['Provider'] +) +@method_decorator(csrf_exempt, name="dispatch") +class RevocationEndpoint(OpBase, View): + + def post(self, request, *args, **kwargs): + try: + self.validate_json_schema( + request.POST.dict(), + "revocation_request", + "Revocation request object validation failed " + ) + self.check_client_assertion( + request.POST['client_id'], + request.POST['client_assertion'] + ) + except ValidationException: # pragma: no cover + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Revocation request object validation failed", + }, + status = 400 + ) + except Exception: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Validation of client assertion failed", + + }, + status = 400 + ) + + access_token = request.POST.get('token', None) + if not access_token: # pragma: no cover + return JsonResponse( + { + "error": "invalid_request", + "error_description": "The request does not include Access Token", + + }, + status = 400 + ) + + token = IssuedToken.objects.filter( + access_token = access_token, + revoked = False + ).first() + + if not token or token.expired: + return JsonResponse( + { + "error": "invalid_grant", + "error_description": "Access Token not found or expired", + + }, + status = 400 + ) + + if token.is_revoked: # pragma: no cover + return JsonResponse( + { + "error": "invalid_grant", + "error_description": "Access Token revoked", + }, + status = 400 + ) + + token.session.revoke() + return HttpResponse() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/token_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/token_endpoint.py new file mode 100644 index 0000000..95f2e81 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/token_endpoint.py @@ -0,0 +1,228 @@ +import base64 +import hashlib +import logging + +from djagger.decorators import schema +from djagger.utils import schema_set_examples +from django.conf import settings +from django.http import ( + HttpResponseBadRequest, + HttpResponseForbidden, + JsonResponse +) +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from pydantic import BaseModel +from spid_cie_oidc.entity.jwtse import unpad_jwt_payload +from spid_cie_oidc.provider.exceptions import ValidationException +from spid_cie_oidc.provider.models import IssuedToken, OidcSession +from spid_cie_oidc.provider.settings import ( + OIDCFED_DEFAULT_PROVIDER_PROFILE, + OIDCFED_PROVIDER_PROFILES, + OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME +) + +from spid_cie_oidc.entity.utils import iat_now +from . import OpBase + +logger = logging.getLogger(__name__) + +schema_profile = OIDCFED_PROVIDER_PROFILES[OIDCFED_DEFAULT_PROVIDER_PROFILE] + + +@schema( + methods=['GET', 'POST'], + post_request_schema={ + "authn_request": schema_profile["authorization_code"], + "refresh_request": schema_profile["refresh_token"], + + }, + post_response_schema={ + "200": schema_profile["authorization_code_response"], + # TODO + # "200": schema_profile["refresh_token_response"], + "400": schema_profile["token_error_response"], + }, + get_response_schema={ + "400": BaseModel + }, + tags=['Provider'] +) +@method_decorator(csrf_exempt, name="dispatch") +class TokenEndpoint(OpBase, View): + """ + Request content type is 'application/x-www-form-urlencoded' + """ + schema = {} + schema["authn_request"] = schema_set_examples({}, schema_profile["authorization_code"]) + schema["refresh_request"] = schema_set_examples({}, schema_profile["refresh_token"]) + + def get(self, request, *args, **kwargs): + return HttpResponseBadRequest() + + def grant_auth_code(self, request, *args, **kwargs): + """ + Token request for an authorization code grant + """ + # PKCE check - based on authz.authz_request["code_challenge_method"] == S256 + code_challenge = hashlib.sha256(request.POST["code_verifier"].encode()).digest() + code_challenge_b64 = base64.urlsafe_b64encode(code_challenge).decode("utf-8") + code_challenge_unpad = code_challenge_b64.replace("=", "") + if code_challenge_unpad != self.authz.authz_request["code_challenge"]: + logger.warning( + f"PCKE validation failed. Produced code challenge [{code_challenge_unpad}]" + f" if different from which sent by RP [{self.authz.authz_request['code_challenge']}]" + ) + return HttpResponseForbidden() + # + + issued_token = IssuedToken.objects.filter( + session=self.authz, + revoked=False + ).first() + + jwk_at = unpad_jwt_payload(issued_token.access_token) + expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp']) + + iss_token_data = dict( # nosec B106 + access_token=issued_token.access_token, + id_token=issued_token.id_token, + token_type="Bearer", # nosec B106 + expires_in=expires_in, + # TODO: remove unsupported scope + scope=self.authz.authz_request["scope"], + ) + if issued_token.refresh_token: + iss_token_data['refresh_token'] = issued_token.refresh_token + return JsonResponse(iss_token_data) + + def is_token_renewable(self, session) -> bool: + provider = getattr(settings, "OIDCFED_PROVIDER_PROFILE") + if provider == "cie": + issuedToken = IssuedToken.objects.filter( + session=session + ).first() + + id_token = unpad_jwt_payload(issuedToken.id_token) + consent_expiration = id_token['iat'] + OIDCFED_PROVIDER_MAX_CONSENT_TIMEFRAME + delta = consent_expiration - iat_now() + if delta > 0: + return True + return False + elif provider == "spid": + # TODO: check also ACR + issuedToken = IssuedToken.objects.filter( + session=session + ) + return ( + (issuedToken.count() - 1) < getattr( + settings, "OIDCFED_PROVIDER_MAX_REFRESH", 1 + ) + ) + + def grant_refresh_token(self, request, *args, **kwargs): + """ + client_id=https://rp.cie.it& + client_assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlNQSUQiLCJhZG1pbiI6dHJ1ZX0.LVyRDPVJm0S9q7oiXcYVIIqGWY0wWQlqxvFGYswL...& + client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer& + refresh_token=8xLOxBtZp8 & + grant_type=refresh_token + """ + # 1. get the IssuedToken refresh one, revoked = None + # 2. create a new instance of issuedtoken linked to the same sessions and revoke the older + # 3. response with a new refresh, access and id_token + issued_token = IssuedToken.objects.filter( + refresh_token=request.POST['refresh_token'], + revoked=False + ).first() + + if not issued_token: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Refresh token not found", + + }, + status=400 + ) + session = issued_token.session + if not self.is_token_renewable(session): # pragma: no cover + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Refresh Token can no longer be updated", + + }, status=400 + ) + iss_token_data = self.get_iss_token_data(session, self.get_issuer()) + IssuedToken.objects.create(**iss_token_data) + issued_token.revoked = True + issued_token.save() + + jwk_at = unpad_jwt_payload(iss_token_data['access_token']) + expires_in = self.get_expires_in(jwk_at['iat'], jwk_at['exp']) + + data = dict( # nosec B106 + access_token=iss_token_data['access_token'], + id_token=iss_token_data['id_token'], + refresh_token=iss_token_data['refresh_token'], + token_type="Bearer", # nosec B106 + expires_in=expires_in + ) + + return JsonResponse(data) + + def post(self, request, *args, **kwargs): + logger.debug(f"{request.headers}: {request.POST}") + try: + self.validate_json_schema( + request.POST.dict(), + request.POST["grant_type"], + "Token request object validation failed " + ) + except ValidationException: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Token request object validation failed", + }, + status=400 + ) + self.commons = self.get_jwt_common_data() + self.issuer = self.get_issuer() + # check client_assertion and client ownership + try: + self.check_client_assertion( + request.POST['client_id'], + request.POST['client_assertion'] + ) + except Exception as e: # pragma: no cover + logger.warning( + "Client authentication failed for " + f"{request.POST.get('client_id', 'unknown')}: {e}" + ) + return JsonResponse( + # TODO: error message here + { + 'error': "unauthorized_client", + 'error_description': "" + + }, status=403 + ) + + if request.POST.get("grant_type") == 'authorization_code': + self.authz = OidcSession.objects.filter( + auth_code=request.POST["code"], + revoked=False + ).first() + + if not self.authz: + return HttpResponseBadRequest() + + return self.grant_auth_code(request) + elif request.POST.get("grant_type") == 'refresh_token': + return self.grant_refresh_token(request) + else: + # Token exchange? :-) + raise NotImplementedError() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/userinfo_endpoint.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/userinfo_endpoint.py new file mode 100644 index 0000000..df10d99 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/views/userinfo_endpoint.py @@ -0,0 +1,106 @@ +import logging + +from django.http import ( + HttpResponse, + HttpResponseForbidden, +) +from djagger.decorators import schema +from django.utils import timezone +from django.views import View +from spid_cie_oidc.entity.jwtse import ( + create_jws, + create_jwe, + unpad_jwt_payload +) + +from spid_cie_oidc.entity.models import ( + TrustChain +) +from spid_cie_oidc.entity.utils import get_jwks, get_key, KeyUsage +from spid_cie_oidc.provider.models import IssuedToken + +from . import OpBase +logger = logging.getLogger(__name__) + + +@schema( + summary="OIDC Provider UserInfo endpoint", + methods=['GET'], + # TODO + # security=[ + # dict( + # description = "", + # name = "", + # bearerFormat = "JWT", + # ) + # ], + external_docs = { + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags = ['Provider'] +) +class UserInfoEndpoint(OpBase, View): + def get(self, request, *args, **kwargs): + """ + Userinfo endpoint just needs a HTTP GET with the Access Token + in a Authorization Http Header like this: + + Authorization: Bearer $JWT + """ + ah = request.headers.get("Authorization", None) + if not ah or "Bearer " not in ah: + return HttpResponseForbidden() + bearer = ah.split("Bearer ")[1] + + token = IssuedToken.objects.filter( + access_token=bearer, + revoked=False, + session__revoked=False, + expires__gte=timezone.localtime(), + ).first() + + if not token: + return HttpResponseForbidden() + + rp_tc = TrustChain.objects.filter( + sub=token.session.client_id, + metadata__openid_relying_party__isnull=False, + is_active=True + ).first() + if not rp_tc: + return HttpResponseForbidden() + + issuer = self.get_issuer() + access_token_data = unpad_jwt_payload(token.access_token) + + # TODO: user claims + jwt = {"sub": access_token_data["sub"]} + for claim in ( + token.session.authz_request.get( + "claims", {} + ).get("userinfo", {}).keys() + ): + if claim in token.session.user.attributes: + jwt[claim] = token.session.user.attributes[claim] + + # sign the data + key = get_key(issuer.jwks_core, KeyUsage.signature) + jws = create_jws(jwt, key) + + # encrypt the data + client_jwks = get_jwks( + rp_tc.metadata['openid_relying_party'], + federation_jwks = rp_tc.jwks + ) + client_jwk = get_key(client_jwks, KeyUsage.encryption) + + jwe = create_jwe( + jws, + client_jwk, + cty="JWT" + ) + return HttpResponse(jwe, content_type="application/jwt") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/admin.py new file mode 100644 index 0000000..a2e0f54 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/admin.py @@ -0,0 +1,78 @@ +import logging + +from django.contrib import admin + +from .models import OidcAuthentication, OidcAuthenticationToken +from .utils import html_json_preview + +logger = logging.getLogger(__name__) + + +class OidcAuthenticationTokenInline(admin.StackedInline): + model = OidcAuthenticationToken + extra = 0 + max_num = 1 + readonly_fields = ( + "scope", + "expires_in", + "token_type", + "code", + "access_token", + "id_token", + "refresh_token", + "access_token_preview", + "id_token_preview", + ) + + +@admin.register(OidcAuthentication) +class OidcAuthenticationAdmin(admin.ModelAdmin): + search_fields = ("endpoint", "state", "client_id", ) + list_display = ("client_id", "state", "endpoint", "created", "modified") + list_filter = ("created", "endpoint") + inlines = (OidcAuthenticationTokenInline,) + readonly_fields = ( + "client_id", + "state", + "endpoint", + "successful", + "json_preview", + "provider_configuration", + "created", + "modified", + ) + exclude = ("issuer_id", "data", "provider_configuration", "provider_jwks") + fieldsets = ( + (None, {"fields": ("provider_id", "client_id", "state", "endpoint")}), + ( + "Status", + { + "fields": ( + "successful", + "created", + "modified", + ) + }, + ), + ( + "Authorization request previews", + { + "fields": ( + "json_preview", + ), + "classes": ("collapse",), + }, + ), + ( + "Provider Discovery result", + { + "fields": ("provider_configuration",), + "classes": ("collapse",), + }, + ), + ) + + def json_preview(self, obj): # pragma: no cover + return html_json_preview(obj.data) + + json_preview.short_description = "Authentication Request data" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/apps.py new file mode 100644 index 0000000..a16fb59 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class SpidCieFederationRpConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.relying_party" + verbose_name = "SPID/CIE OIDC Federation Relying Party" + label = "spid_cie_oidc_relying_party" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/exceptions.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/exceptions.py new file mode 100644 index 0000000..2e3be02 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/exceptions.py @@ -0,0 +1,2 @@ +class ValidationException(Exception): + pass diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/fetch_openid_providers.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/fetch_openid_providers.py new file mode 100644 index 0000000..3517f2b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/management/commands/fetch_openid_providers.py @@ -0,0 +1,66 @@ +import logging + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.utils.translation import gettext as _ + +from spid_cie_oidc.entity.settings import HTTPC_PARAMS +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain + + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Attribute release query" + + def add_arguments(self, parser): + parser.epilog = "Example: ./manage.py fetch_openid_providers" + parser.add_argument( + "--start", + action="store_true", + required=True, + help=_( + "Collect a trust chains for each openid_provider defined in " + "settings.OIDCFED_IDENTITY_PROVIDERS" + ), + ) + parser.add_argument( + "-f", + "--force", + action="store_true", + required=False, + help=_("Don't use already cached statements and chains"), + ) + parser.add_argument( + "-debug", required=False, action="store_true", help="see debug message" + ) + + def handle(self, *args, **options): + if not options["start"]: + return # pragma: no cover + + res = [] + for op_profile in settings.OIDCFED_IDENTITY_PROVIDERS.keys(): + for op_sub, ta in settings.OIDCFED_IDENTITY_PROVIDERS[op_profile].items(): + logger.info(f"Fetching Entity Configuration for {op_sub}") + try: + tc = get_or_create_trust_chain( + subject=op_sub, + trust_anchor=ta, + httpc_params=HTTPC_PARAMS, + required_trust_marks=getattr( + settings, "OIDCFED_REQUIRED_TRUST_MARKS", [] + ), + force=options["force"], + ) + if tc.is_valid: + res.append(tc) + + logger.info(f"Final Metadata for {tc.sub}:\n\n{tc.metadata}") + + except Exception as e: + logger.error(f"Failed to download {op_sub} due to: {e}") + continue + + logger.info(f"Found {res}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0001_initial.py new file mode 100644 index 0000000..c23ed60 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0001_initial.py @@ -0,0 +1,90 @@ +# Generated by Django 4.0.2 on 2022-02-13 09:06 + +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="OidcAuthentication", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("client_id", models.CharField(max_length=255)), + ( + "state", + models.CharField( + default="state-is-unique", max_length=255, unique=True + ), + ), + ("endpoint", models.URLField(blank=True, null=True)), + ("issuer", models.CharField(blank=True, max_length=255, null=True)), + ("issuer_id", models.CharField(blank=True, max_length=255, null=True)), + ("provider_jwks", models.TextField(blank=True, null=True)), + ("data", models.TextField(blank=True, null=True)), + ("successful", models.BooleanField(default=False)), + ("provider_configuration", models.TextField(blank=True, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ], + options={ + "verbose_name": "OIDC Authentication", + "verbose_name_plural": "OIDC Authentications", + }, + ), + migrations.CreateModel( + name="OidcAuthenticationToken", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("code", models.CharField(blank=True, max_length=255, null=True)), + ("access_token", models.TextField(blank=True, null=True)), + ("id_token", models.TextField(blank=True, null=True)), + ("scope", models.CharField(blank=True, max_length=255, null=True)), + ("token_type", models.CharField(blank=True, max_length=255, null=True)), + ("expires_in", models.IntegerField(blank=True, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("logged_out", models.DateTimeField(blank=True, null=True)), + ( + "authz_request", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="spid_cie_oidc_relying_party.oidcauthentication", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0002_rename_issuer_oidcauthentication_provider_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0002_rename_issuer_oidcauthentication_provider_and_more.py new file mode 100644 index 0000000..7ef16b1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0002_rename_issuer_oidcauthentication_provider_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.2 on 2022-03-03 11:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("spid_cie_oidc_relying_party", "0001_initial"), + ] + + operations = [ + migrations.RenameField( + model_name="oidcauthentication", + old_name="issuer", + new_name="provider", + ), + migrations.RenameField( + model_name="oidcauthentication", + old_name="issuer_id", + new_name="provider_id", + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0003_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0003_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py new file mode 100644 index 0000000..7f2da0a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0003_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.2 on 2022-03-07 08:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0002_rename_issuer_oidcauthentication_provider_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='oidcauthenticationtoken', + old_name='logged_out', + new_name='revoked', + ), + migrations.AddField( + model_name='oidcauthenticationtoken', + name='refresh_token', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0004_rename_revoked_oidcauthenticationtoken_logged_out_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0004_rename_revoked_oidcauthenticationtoken_logged_out_and_more.py new file mode 100644 index 0000000..7cc524c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0004_rename_revoked_oidcauthenticationtoken_logged_out_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.2 on 2022-03-07 10:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0003_rename_logged_out_oidcauthenticationtoken_revoked_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='oidcauthenticationtoken', + old_name='revoked', + new_name='logged_out', + ), + migrations.RemoveField( + model_name='oidcauthenticationtoken', + name='refresh_token', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0005_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0005_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py new file mode 100644 index 0000000..cfffd72 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0005_rename_logged_out_oidcauthenticationtoken_revoked_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.2 on 2022-03-07 11:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0004_rename_revoked_oidcauthenticationtoken_logged_out_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='oidcauthenticationtoken', + old_name='logged_out', + new_name='revoked', + ), + migrations.AddField( + model_name='oidcauthenticationtoken', + name='refresh_token', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='oidcauthentication', + name='provider_configuration', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0006_alter_oidcauthentication_provider_configuration_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0006_alter_oidcauthentication_provider_configuration_and_more.py new file mode 100644 index 0000000..44aa9a8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0006_alter_oidcauthentication_provider_configuration_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.2 on 2022-03-07 11:27 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0005_rename_logged_out_oidcauthenticationtoken_revoked_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oidcauthentication', + name='provider_configuration', + field=models.JSONField(blank=True, default=dict, null=True), + ), + migrations.AlterField( + model_name='oidcauthentication', + name='state', + field=models.CharField(default=uuid.uuid4, max_length=255, unique=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0007_alter_oidcauthentication_provider_jwks.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0007_alter_oidcauthentication_provider_jwks.py new file mode 100644 index 0000000..cb08e62 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0007_alter_oidcauthentication_provider_jwks.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.2 on 2022-03-07 11:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0006_alter_oidcauthentication_provider_configuration_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oidcauthentication', + name='provider_jwks', + field=models.JSONField(blank=True, default=dict, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0008_remove_oidcauthentication_provider_and_more.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0008_remove_oidcauthentication_provider_and_more.py new file mode 100644 index 0000000..95c18fd --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0008_remove_oidcauthentication_provider_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.3 on 2022-04-10 19:36 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0007_alter_oidcauthentication_provider_jwks'), + ] + + operations = [ + migrations.RemoveField( + model_name='oidcauthentication', + name='provider', + ), + migrations.RemoveField( + model_name='oidcauthentication', + name='provider_jwks', + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0009_alter_oidcauthenticationtoken_code.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0009_alter_oidcauthenticationtoken_code.py new file mode 100644 index 0000000..a68edc4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/0009_alter_oidcauthenticationtoken_code.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2026-02-02 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party', '0008_remove_oidcauthentication_provider_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='oidcauthenticationtoken', + name='code', + field=models.CharField(blank=True, max_length=2048, null=True), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/models.py new file mode 100644 index 0000000..4a7753b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/models.py @@ -0,0 +1,80 @@ +import json +import logging +import uuid + +from django.contrib.auth import get_user_model +from django.db import models + +from spid_cie_oidc.entity.jwtse import unpad_jwt_payload + + +logger = logging.getLogger(__name__) + + +class OidcAuthentication(models.Model): + client_id = models.CharField(max_length=255) + state = models.CharField(max_length=255, unique=True, default=uuid.uuid4) + endpoint = models.URLField(blank=True, null=True) + data = models.TextField(blank=True, null=True) + successful = models.BooleanField(default=False) + + provider_id = models.CharField(max_length=255, blank=True, null=True) + provider_configuration = models.JSONField( + blank=True, null=True, default=dict + ) + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "OIDC Authentication" + verbose_name_plural = "OIDC Authentications" + + def __str__(self): + return f"{self.client_id} {self.state} to {self.endpoint}" + + +class OidcAuthenticationToken(models.Model): + user = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, blank=True, null=True + ) + authz_request = models.ForeignKey(OidcAuthentication, on_delete=models.CASCADE) + code = models.CharField(max_length=2048, blank=True, null=True) + access_token = models.TextField(blank=True, null=True) + id_token = models.TextField(blank=True, null=True) + refresh_token = models.TextField(blank=True, null=True) + + scope = models.CharField(max_length=255, blank=True, null=True) + token_type = models.CharField(max_length=255, blank=True, null=True) + expires_in = models.IntegerField(blank=True, null=True) + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + revoked = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return f"{self.authz_request} {self.code}" + + def token_preview(self, token): + if token: + try: + data = unpad_jwt_payload(token) + dumps = json.dumps(data, indent=2) + return dumps # mark_safe(dumps.replace("\n", "
").replace(" ", " ")) + except Exception as e: + logger.error(e) + else: + return '--' + + @property + def access_token_preview(self): + return self.token_preview(self.access_token) + + @property + def id_token_preview(self): + return self.token_preview(self.id_token) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.authz_request.successful = True + self.authz_request.save() diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oauth2/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oauth2/__init__.py new file mode 100644 index 0000000..2ed5012 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oauth2/__init__.py @@ -0,0 +1,73 @@ +import json +import logging +import requests +import uuid + +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.settings import HTTPC_PARAMS, HTTPC_TIMEOUT +from spid_cie_oidc.entity.utils import iat_now, exp_from_now, get_key, KeyUsage + +logger = logging.getLogger(__name__) + + +class OAuth2AuthorizationCodeGrant(object): + """ + https://tools.ietf.org/html/rfc6749 + """ + + def access_token_request( + self, + redirect_uri: str, + state: str, + code: str, + issuer_id: str, + client_conf: FederationEntityConfiguration, + token_endpoint_url: str, + audience: list, + code_verifier: str = None, + ): + """ + Access Token Request + https://tools.ietf.org/html/rfc6749#section-4.1.3 + """ + grant_data = dict( + grant_type="authorization_code", + redirect_uri=redirect_uri, + client_id=client_conf.sub, + state=state, + code=code, + code_verifier=code_verifier, + # here private_key_jwt + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion=create_jws( + { + "iss": client_conf.sub, + "sub": client_conf.sub, + "aud": [token_endpoint_url], + "iat": iat_now(), + "exp": exp_from_now(), + "jti": str(uuid.uuid4()), + }, + jwk_dict=get_key(client_conf.jwks_core, KeyUsage.signature), + ), + ) + + logger.debug(f"Access Token Request for {state}: {grant_data} ") + token_request = requests.post( + token_endpoint_url, + data=grant_data, + verify=HTTPC_PARAMS["connection"]["ssl"], + timeout=HTTPC_TIMEOUT, + ) + + if token_request.status_code != 200: # pragma: no cover + logger.error( + f"Something went wrong with {state}: {token_request.status_code}" + ) + else: + try: + token_request = json.loads(token_request.content.decode()) + except Exception as e: # pragma: no cover + logger.error(f"Something went wrong with {state}: {e}") + return token_request diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oidc/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oidc/__init__.py new file mode 100644 index 0000000..9f55b7c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/oidc/__init__.py @@ -0,0 +1,79 @@ +import logging +import requests + +from django.conf import settings +from spid_cie_oidc.entity.exceptions import UnknownKid +from spid_cie_oidc.entity.jwtse import unpad_jwt_head, decrypt_jwe, verify_jws +from spid_cie_oidc.entity.utils import get_jwks + +logger = logging.getLogger(__name__) + + +class OidcUserInfo(object): + """ + https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + """ + + def get_jwk(self, kid, jwks): + for jwk in jwks: + if jwk.get("kid", None) and jwk["kid"] == kid: + return jwk + raise UnknownKid() # pragma: no cover + + def get_userinfo( + self, state: str, access_token: str, provider_conf: dict, verify: bool + ): + """ + User Info endpoint request with bearer access token + """ + # userinfo + headers = {"Authorization": f"Bearer {access_token}"} + authz_userinfo = requests.get( + provider_conf["userinfo_endpoint"], + headers=headers, + verify=verify, + timeout=getattr( + settings, "HTTPC_TIMEOUT", 8 + ) # nosec - B113 + ) + + if authz_userinfo.status_code != 200: # pragma: no cover + logger.error( + f"Something went wrong with {state}: {authz_userinfo.status_code}" + ) + return False + else: + try: + # if application/json ... let it be + return authz_userinfo.json() + except Exception: + logger.debug("userinfo response is not in plain json") + + try: + jwe = authz_userinfo.content.decode() + + header = unpad_jwt_head(jwe) + # header["kid"] kid di rp + rp_jwk = self.get_jwk(header["kid"], self.rp_conf.jwks_core) + jws = decrypt_jwe(jwe, rp_jwk) + + if isinstance(jws, bytes): + jws = jws.decode() + + header = unpad_jwt_head(jws) + idp_jwks = get_jwks(provider_conf) + idp_jwk = self.get_jwk(header["kid"], idp_jwks) + + decoded_jwt = verify_jws(jws, idp_jwk) + logger.debug(f"Userinfo endpoint result: {decoded_jwt}") + return decoded_jwt + + except KeyError as e: # pragma: no cover + logger.error(f"Userinfo response error {state}: {e}") + return False + except UnknownKid as e: + logger.error(f"Userinfo Unknow KID for session {state}: {e}") + return False + except Exception as e: # pragma: no cover + logger.error(f"Userinfo response unknown error {state}: {e}") + return False diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/processors.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/processors.py new file mode 100644 index 0000000..7d2677a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/processors.py @@ -0,0 +1,4 @@ +def issuer_prefixed_sub( + user_info: dict, client_conf: dict, data: dict = None, kwargs: dict = {} +): + return f"{client_conf['provider_id']}{data.get('sep', '__')}{user_info['sub']}" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/settings.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/settings.py new file mode 100644 index 0000000..800d94a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/settings.py @@ -0,0 +1,124 @@ +from django.conf import settings +from spid_cie_oidc.provider.schemas.authn_requests import ( + AcrValues, + AuthenticationRequestCie, + AuthenticationRequestDoc, + AuthenticationRequestSpid, +) +from spid_cie_oidc.entity.schemas.rp_metadata import RPMetadataSpid, RPMetadataCie +from spid_cie_oidc.provider.schemas.authn_response import AuthenticationResponse, AuthenticationResponseCie +from spid_cie_oidc.provider.schemas.token_response import TokenResponse + + +RP_PREFS = { + "application_name": "that_fancy_rp", + "application_type": "web", + "contacts": ["ops@example.com"], + "response_types": ["code"], + "scope": ["openid", "offline_access"], + "token_endpoint_auth_method": ["private_key_jwt"], +} + + +OIDCFED_ACR_PROFILES = getattr( + settings, + "OIDCFED_ACR_PROFILES", + AcrValues.l2.value +) + +RP_PROVIDER_PROFILES = getattr( + settings, + "RP_PROVIDER_PROFILES", + { + "spid": { + "authorization_request": AuthenticationRequestSpid, + "authorization_request_doc": AuthenticationRequestDoc, + "rp_metadata": RPMetadataSpid, + "authn_response": AuthenticationResponse, + "token_response": TokenResponse + }, + "cie": { + "authorization_request": AuthenticationRequestCie, + "authorization_request_doc": AuthenticationRequestDoc, + "rp_metadata": RPMetadataCie, + "authn_response": AuthenticationResponseCie, + "token_response": TokenResponse + }, + }, +) + +RP_USER_LOOKUP_FIELD = getattr(settings, "RP_USER_LOOKUP_FIELD", "fiscal_number") +RP_USER_CREATE = getattr(settings, "RP_USER_CREATE", True) + +RP_ATTR_MAP = getattr( + settings, + "RP_ATTR_MAP", + { + "sub": ("sub",), + "username": ( + { + "func": "spid_cie_oidc.relying_party.processors.issuer_prefixed_sub", + "kwargs": {"sep": "__"}, + }, + ), + "first_name": ("given_name", "given_name"), + "last_name": ( + "family_name", + "last_name", + ), + "email": ( + "email", + "email", + ), + "fiscal_number": ("https://attributes.eid.gov.it/fiscal_number", "fiscal_number"), + }, +) + + +SPID_REQUESTED_CLAIMS = getattr( + settings, + "RP_REQUIRED_CLAIMS", + { + "id_token": { + "given_name": {"essential": True}, + "email": {"essential": True}, + }, + "userinfo": { + "given_name": None, + "family_name": None, + "email": None, + "https://attributes.eid.gov.it/fiscal_number": None, + }, + }, +) + +CIE_REQUESTED_CLAIMS = getattr( + settings, + "RP_REQUIRED_CLAIMS", + { + "id_token": {"family_name": {"essential": True}, "given_name": {"essential": True}}, + "userinfo": { + "given_name": None, + "family_name": None, + "email": None, + "https://attributes.eid.gov.it/fiscal_number": None + }, + }, +) + +RP_PKCE_CONF = getattr( + settings, + "RP_PKCE_CONF", + { + "function": "spid_cie_oidc.relying_party.utils.get_pkce", + "kwargs": {"code_challenge_length": 64, "code_challenge_method": "S256"}, + }, +) + +RP_REQUEST_CLAIM_BY_PROFILE = { + "spid": SPID_REQUESTED_CLAIMS, + "cie": CIE_REQUESTED_CLAIMS, +} + +RP_DEFAULT_PROVIDER_PROFILES = getattr(settings, "RP_DEFAULT_PROVIDER_PROFILES", "spid") +RP_REQUEST_EXP = getattr(settings, "RP_REQUEST_EXP", 60) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/css/access-button.css b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/css/access-button.css new file mode 100644 index 0000000..bcefaa6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/css/access-button.css @@ -0,0 +1,413 @@ +@import url(https://fonts.googleapis.com/css?family=Titillium+Web:400,600); +.italia-it-button { + display: inline-block; + position: relative; + padding: 0; + color: #FFF; + font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-weight: 600; + line-height: 1em; + text-decoration: none; + border: 0; + text-align: center; + cursor: pointer; + overflow: hidden; + border-radius: 15px; +} + +.italia-it-button-icon, +.italia-it-button-text { + display: block; + float: left +} + +.italia-it-button-icon { + margin: 0 -.4em 0 0; + padding: 0.6em .8em .5em; + border-right: rgba(255, 255, 255, 0.1) 0.1em solid +} + +.italia-it-button-text { + padding: .95em 1em .85em 1em; + font-size: 1.15em; + text-align: center +} + +svg { + width: 1.8em; + height: 1.8em; + fill: #fff +} + +.italia-it-block { + display: block +} + +.italia-it-button-size-s { + font-size: 10px; + width: 150px; + border-radius: 5px; +} + +.italia-it-button-size-s > span img { + width: 19px; + height: 19px; + border: 0 +} + +.italia-it-button-size-m { + font-size: 15px; + width: 220px; + border-radius: 0px; +} + +.italia-it-button-size-m > span img { + width: 29px; + height: 29px; + border: 0 +} + +.italia-it-button-size-l { + font-size: 20px; + width: 280px +} + +.italia-it-button-size-l > span img { + width: 38px; + height: 38px; + border: 0 +} + +.italia-it-button-size-xl { + font-size: 24px; + width: 340px; +} + +.italia-it-button-size-xl > span img { + width: 47px; + height: 47px; + border: 0 +} + +.button-spid { + background-color: #06C; + color: #FFF +} + +.button-spid svg { + fill: #FFF +} + +.button-spid:hover { + background-color: #036; + color: #FFF +} + +.button-spid:active { + background-color: #83BEED; + color: #036 +} + +.button-cie { + background-color: #4c5c6c; + color: #FFF +} + +.button-cie svg { + fill: #FFF +} + +.button-cie:hover { + background-color: #414c58; + color: #FFF +} + +.button-cie:active { + background-color: #394046; + color: #036 +} + +.spid-idp-button { + position: absolute; + z-index: 1039; + display: none +} + +.spid-idp-button .spid-idp-button-menu, +.spid-idp-button .spid-idp-button-panel { + list-style: none; + background: white; + border: solid 1px #ddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + overflow: visible; + padding: 0; + margin: 0 +} + +#spid-idp-button-small-get, +#spid-idp-button-medium-get, +#spid-idp-button-small-post, +#spid-idp-button-medium-post { + width: 230px +} + +#spid-idp-button-large-get, +#spid-idp-button-large-post { + width: 270px +} + +#spid-idp-button-xlarge-get, +#spid-idp-button-xlarge-post { + width: 330px +} + +.spid-idp-button .spid-idp-button-panel { + padding: 10px +} + +.spid-idp-button.spid-idp-button-tip { + margin-top: 8px +} + +.spid-idp-button.spid-idp-button-tip:before { + position: absolute; + top: -6px; + left: 9px; + content: ""; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ddd; + display: inline-block +} + +.spid-idp-button.spid-idp-button-tip:after { + position: absolute; + top: -5px; + left: 10px; + content: ""; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + display: inline-block +} + +.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:before { + left: auto; + right: 9px +} + +.spid-idp-button.spid-idp-button-tip.spid-idp-button-anchor-right:after { + left: auto; + right: 10px +} + +.spid-idp-button.spid-idp-button-scroll .spid-idp-button-menu, +.spid-idp-button.spid-idp-button-scroll .spid-idp-button-panel { + max-height: 180px; + overflow: auto +} + +.spid-idp-button .spid-idp-button-menu li { + list-style: none; + padding: 0 0; + margin: 0; + line-height: 18px +} + +.spid-idp-button .spid-idp-button-menu li > a, +.spid-idp-button .spid-idp-button-menu label { + display: block; + font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-weight: 600; + font-size: .9em; + color: #3b61b6; + text-decoration: underline; + line-height: 18px; + padding-top: 5px; + white-space: nowrap; + border-bottom: 1px solid #ddd; +} + +.spid-idp-button .spid-idp-button-menu li > a:hover, +.spid-idp-button .spid-idp-button-menu label:hover { + color: #036; + cursor: pointer; + background-color: #F0F0F0 +} + +.spid-idp-button .spid-idp-button-menu li > a img { + height: 35px; + padding-bottom: 5px; + border: 0; + width: 100%; +} + +.idp-button-idp-logo { + font-size: 100%; + height: 10%; + width: 100%; + border: 0; + border-bottom: 1px solid #CCC; + background-color: #FFF; + padding: 15px; + text-align: left; + cursor: pointer +} + +.idp-button-idp-logo:hover { + background-color: #F0F0F0 +} + +.idp-button-idp-logo img { + height: 25px; + vertical-align: middle; + cursor: pointer; +} + +.spid-top-menu-space-10 > a img { + margin-top: 10px +} + +.spid-top-menu-space-20 > a img { + margin-top: 20px +} + +.spid-top-menu-line { + border-top: 5px solid #000 +} + +.spid-idp-support-link > a { + padding: 5px 0 10px 10px +} + +.spid-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0 +} + +.cie-idp-button { + position: absolute; + z-index: 1039; + display: none +} + +.cie-idp-button .cie-idp-button-menu, +.cie-idp-button .cie-idp-button-panel { + list-style: none; + background: white; + border: solid 1px #ddd; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + overflow: visible; + padding: 0; + margin: 0 +} + +#cie-idp-button-small-get, +#cie-idp-button-medium-get, +#cie-idp-button-small-post, +#cie-idp-button-medium-post { + width: 230px +} + +#cie-idp-button-large-get, +#cie-idp-button-large-post { + width: 270px +} + +#cie-idp-button-xlarge-get, +#cie-idp-button-xlarge-post { + width: 330px +} + +.cie-idp-button .cie-idp-button-panel { + padding: 10px +} + +.cie-idp-button.cie-idp-button-tip { + margin-top: 8px +} + +.cie-idp-button.cie-idp-button-tip:before { + position: absolute; + top: -6px; + left: 9px; + content: ""; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ddd; + display: inline-block +} + +.cie-idp-button.cie-idp-button-tip:after { + position: absolute; + top: -5px; + left: 10px; + content: ""; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + display: inline-block +} + +.cie-idp-button.cie-idp-button-tip.cie-idp-button-anchor-right:before { + left: auto; + right: 9px +} + +.cie-idp-button.cie-idp-button-tip.cie-idp-button-anchor-right:after { + left: auto; + right: 10px +} + +.cie-idp-button.cie-idp-button-scroll .cie-idp-button-menu, +.cie-idp-button.cie-idp-button-scroll .cie-idp-button-panel { + max-height: 180px; + overflow: auto +} + +.cie-idp-button .cie-idp-button-menu li { + list-style: none; + padding: 0 0; + margin: 0; + line-height: 18px +} + +.cie-idp-button .cie-idp-button-menu li > a, +.cie-idp-button .cie-idp-button-menu label { + display: block; + font-family: "Titillium Web", HelveticaNeue, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; + font-weight: 600; + font-size: .9em; + color: #3b61b6; + text-decoration: underline; + line-height: 18px; + padding-top: 5px; + white-space: nowrap; + border-bottom: 1px solid #ddd; +} + +.cie-idp-button .cie-idp-button-menu li > a:hover, +.cie-idp-button .cie-idp-button-menu label:hover { + color: #036; + cursor: pointer; + background-color: #F0F0F0 +} + +.cie-idp-button .cie-idp-button-menu li > a img { + height: 35px; + padding-bottom: 5px; + border: 0; + width: 30%; +} + + + + + + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid-sp-access-button.js b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid-sp-access-button.js new file mode 100644 index 0000000..86f9584 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid-sp-access-button.js @@ -0,0 +1,78 @@ +jQuery && function (t) { + function i(i, n) { + var d = i ? t(this) : n, o = t(d.attr("spid-idp-button")), r = d.hasClass("spid-idp-button-open") + if (i) { + if (t(i.target).hasClass("spid-idp-button-ignore")) return + i.preventDefault(), i.stopPropagation() + } else if (d !== n.target && t(n.target).hasClass("spid-idp-button-ignore")) return + s(), r || d.hasClass("spid-idp-button-disabled") || (d.addClass("spid-idp-button-open"), o.data("spid-idp-button-trigger", d).show(), e(), o.trigger("show", { spidIDPButton: o, trigger: d })) + } + + function s(i) { + var s = i ? t(i.target).parents().addBack() : null + if (s && s.is(".spid-idp-button")) { + if (!s.is(".spid-idp-button-menu")) return + if (!s.is("A")) return + } + t(document).find(".spid-idp-button:visible").each(function () { + var i = t(this) + i.hide().removeData("spid-idp-button-trigger").trigger("hide", { spidIDPButton: i }) + }), t(document).find(".spid-idp-button-open").removeClass("spid-idp-button-open") + } + + function e() { + var i = t(".spid-idp-button:visible").eq(0), s = i.data("spid-idp-button-trigger"), e = s ? parseInt(s.attr("data-horizontal-offset") || 0, 10) : null, n = s ? parseInt(s.attr("data-vertical-offset") || 0, 10) : null + 0 !== i.length && s && (i.hasClass("spid-idp-button-relative") ? i.css({ left: i.hasClass("spid-idp-button-anchor-right") ? s.position().left - (i.outerWidth(!0) - s.outerWidth(!0)) - parseInt(s.css("margin-right"), 10) + e : s.position().left + parseInt(s.css("margin-left"), 10) + e, top: s.position().top + s.outerHeight(!0) - parseInt(s.css("margin-top"), 10) + n }) : i.css({ left: i.hasClass("spid-idp-button-anchor-right") ? s.offset().left - (i.outerWidth() - s.outerWidth()) + e : s.offset().left + e, top: s.offset().top + s.outerHeight() + n })) + } t.extend(t.fn, { + spidIDPButton: function (e, n) { + switch (e) { + case "show": return i(null, t(this)), t(this) + case "hide": return s(), t(this) + case "attach": return t(this).attr("spid-idp-button", n) + case "detach": return s(), t(this).removeAttr("spid-idp-button") + case "disable": return t(this).addClass("spid-idp-button-disabled") + case "enable": return s(), t(this).removeClass("spid-idp-button-disabled") + } + } + }), t(document).on("click.spid-idp-button", "[spid-idp-button]", i), t(document).on("click.spid-idp-button", s), t(window).on("resize", e) +}(jQuery) + + +jQuery && function (t) { + function i(i, n) { + var d = i ? t(this) : n, o = t(d.attr("cie-idp-button")), r = d.hasClass("cie-idp-button-open") + if (i) { + if (t(i.target).hasClass("cie-idp-button-ignore")) return + i.preventDefault(), i.stopPropagation() + } else if (d !== n.target && t(n.target).hasClass("cie-idp-button-ignore")) return + s(), r || d.hasClass("cie-idp-button-disabled") || (d.addClass("cie-idp-button-open"), o.data("cie-idp-button-trigger", d).show(), e(), o.trigger("show", { cieIDPButton: o, trigger: d })) + } + + function s(i) { + var s = i ? t(i.target).parents().addBack() : null + if (s && s.is(".cie-idp-button")) { + if (!s.is(".cie-idp-button-menu")) return + if (!s.is("A")) return + } + t(document).find(".cie-idp-button:visible").each(function () { + var i = t(this) + i.hide().removeData("cie-idp-button-trigger").trigger("hide", { cieIDPButton: i }) + }), t(document).find(".cie-idp-button-open").removeClass("cie-idp-button-open") + } + + function e() { + var i = t(".cie-idp-button:visible").eq(0), s = i.data("cie-idp-button-trigger"), e = s ? parseInt(s.attr("data-horizontal-offset") || 0, 10) : null, n = s ? parseInt(s.attr("data-vertical-offset") || 0, 10) : null + 0 !== i.length && s && (i.hasClass("cie-idp-button-relative") ? i.css({ left: i.hasClass("cie-idp-button-anchor-right") ? s.position().left - (i.outerWidth(!0) - s.outerWidth(!0)) - parseInt(s.css("margin-right"), 10) + e : s.position().left + parseInt(s.css("margin-left"), 10) + e, top: s.position().top + s.outerHeight(!0) - parseInt(s.css("margin-top"), 10) + n }) : i.css({ left: i.hasClass("cie-idp-button-anchor-right") ? s.offset().left - (i.outerWidth() - s.outerWidth()) + e : s.offset().left + e, top: s.offset().top + s.outerHeight() + n })) + } t.extend(t.fn, { + cieIDPButton: function (e, n) { + switch (e) { + case "show": return i(null, t(this)), t(this) + case "hide": return s(), t(this) + case "attach": return t(this).attr("cie-idp-button", n) + case "detach": return s(), t(this).removeAttr("cie-idp-button") + case "disable": return t(this).addClass("cie-idp-button-disabled") + case "enable": return s(), t(this).removeClass("cie-idp-button-disabled") + } + } + }), t(document).on("click.cie-idp-button", "[cie-idp-button]", i), t(document).on("click.cie-idp-button", s), t(window).on("resize", e) +}(jQuery) \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid_button.js b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid_button.js new file mode 100644 index 0000000..a96c57c --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/js/spid_button.js @@ -0,0 +1,81 @@ +$(document).ready(function(){ + var rootList = $("#spid-idp-list-small-root-get"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-medium-root-get"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#cie-idp-list-medium-root-get"); + var idpList = rootList.children(".cie-idp-button-link"); + var lnkList = rootList.children(".cie-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-large-root-get"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-xlarge-root-get"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-small-root-post"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-medium-root-post"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-large-root-post"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); +$(document).ready(function(){ + var rootList = $("#spid-idp-list-xlarge-root-post"); + var idpList = rootList.children(".spid-idp-button-link"); + var lnkList = rootList.children(".spid-idp-support-link"); + while (idpList.length) { + rootList.append(idpList.splice(Math.floor(Math.random() * idpList.length), 1)[0]); + } + rootList.append(lnkList); +}); \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb-LTS.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb-LTS.svg new file mode 100644 index 0000000..45fe193 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb-LTS.svg @@ -0,0 +1,37 @@ + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Long Term Session + + \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb.svg new file mode 100644 index 0000000..be1851a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/cie-ico-circle-bb.svg @@ -0,0 +1 @@ +Risorsa 2 \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/favicon.ico b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f63204c4e52ee39b23789246447e2377569b8019 GIT binary patch literal 7406 zcmeHM2~bsK6#m#`dGJ^s2%@a=5Rl8_LqNq85q;=Wuloi{YOc8NrrBs(W4YzzWH~iv zZj+i@PNq}NWR}W~VuC0r8d`!2vI=zm|1CX)r__{=(>pWo-T(j3_nrISbMNJy?*N#< z3}IoQqZRf_fk*(}-kP};aE;a+9Mtov7Qk9pAb>DLK{9<9kDImnz`sq@<3G;sK=x8E zTv`>1x|#~4r`RBCp*zwj(8J58kTJ^+*^51J@BAKIm;^k$@&hvFI1sjyuoC=sd<}Bm zY>%?^1E_yeLwG65G7sVQvDJhz+BD8_n}Uq6rZnG#(4bQJEL+l4C|;wVNM z?ws0$>?NKky|5P*H#1TFM22+c-34p!p0W8SPcifP|Ssa}^4SA|NC{F;-&esua$P z5)u+d4^tBqUXk4fkMA9nGT?Os1WEKr*{D&8LwP`jsjW{kH(RA4{Ok@4ATYdQ+%z>h zIoYyv3-FTBW?H=|wb-$kL&z79Nv^dMSK zOiWH(xPX?M;#~p<4H~4Js)4Ex4(!GxcWoBmVQx%J>^yUQe0L9yZyVBFeC~>{(#I>u z@I`;ac)v#YBzKN)*X(|{t1({T=-MWEpp%^8gTun-&!3#a-beNGHe_a>J}U3-`uj;z zSoHOs5lHZ)q>v~J3yY5Y^;Y989pglksDMWJB;Ea@Fj>f)>qL5QJJ*y7J=6LcCgYoQx1z7;_HXr0c?oGarRv$p&m*Ouui)?8Dj&#>iM^6J*XC^@-- z>&Q!Q2h*M{japLsEEp)7 z3heqt|5)?!+6)PUqQ{yDDxo_yYZC?T59nvB7@*6qKt`Jplu9MR7(XbS4VyLYkMGp1 zk1YyuHvK(44Et5VzI`OlgAMXkDyg*T&3y*^rMh0)A0Ew#S)D>Hc3!LK>J&|F!XcV5 z>+RN*74zOO>!;@|^PwL3TK;>Uw<#WlUreGJL`J>rw$#@>!F$xK2h94~te4Ju+N?)> z``9Yp&%XN6K-@aAl6t#&yf4n`64n#X+pOgE4*TuTnBz$2lkwhk(XMH{2haNMB`4QY zP4bZU?MqIq<26HR+E=_^&VJWf&BE%3%=xWRc{`Wa1FT-Sv3C})hpO)8BWJms_u^R% zQd#mV?^Uy&J*zd0X3T&w1OFrga=8pL_N6x8U~gA>m`i(0%{;n`S!8s8T_=AHfaY!< zAztn_fBD@U0^51Y*<2=Jo4>s@UXkyRi&JuTocQ6-P i`*r6377Psv1wv)EfgxSVtu>jcg;Oi-{ \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/echo_attributes.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/echo_attributes.html new file mode 100644 index 0000000..5242637 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/echo_attributes.html @@ -0,0 +1,75 @@ +{% extends 'rp_base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} + +
+ {% csrf_token %} +

+ {% trans "User attributes" %} +

+ +
+ {% for attribute, value in oidc_rp_user_attrs.items %} +
{{ attribute }}:
+
{{ value }}
+ {% endfor %} +
+ + Log out + Verify Access Token (Introspection) + +
+{% if rt_expiration == 0 %} +
+

{% trans "Warning" %}:

+

{% trans 'To enable revocable long sessions you must set OIDCFED_ACR_PROFILES in settingslocal.py of the Relying Party' %} OIDCFED_ACR_PROFILES=["https://www.spid.gov.it/SpidL2","https://www.spid.gov.it/SpidL1"]

+
+{% else %} +
+

ACCESS TOKEN

+

{% trans "The" %} access token {{ at_jti }}
+ {% trans "expires in" %} {{ at_expiration }}

+
+
+

REFRESH TOKEN {% trans "Extend Session" %}

+ +

{% trans "The" %} Refresh Token {{ rt_jti }}
+ {% trans "expires in" %} {{ rt_expiration }}

+ +
+{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_base.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_base.html new file mode 100644 index 0000000..02c74c6 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_base.html @@ -0,0 +1,92 @@ + +{% extends 'bootstrap-italia-base.html' %} + +{% load static %} +{% load i18n %} + +{% block extra_head %} + {{ block.super }} + +{% endblock extra_head %} + +{% block page_title %} +{% trans "OIDC Relying Party Django" %} +{% endblock page_title %} + + +{% block header_slim_org_url %} +https://www.spid.gov.it/ +{% endblock header_slim_org_url %} + + +{% block header_slim_org_name %} + {% trans "OIDC Relying Party" %} +{% endblock header_slim_org_name %} + + +{% block header_slim_mobile_org_name %} + {% trans "OIDC Relying Party" %} +{% endblock header_slim_mobile_org_name %} + + +{% block header_mobile_arrow %}{% endblock header_mobile_arrow %} +{% block header_mobile_slim_menu %}{% endblock header_mobile_slim_menu %} + + +{% block header_center_logo %} +{% endblock header_center_logo %} + + + +{% block header_center_org_name %} +{% trans "OIDC Relying Party" %} +{% endblock header_center_org_name %} + +{% block main_menu %}{% endblock main_menu %} + + +{% block header_center_org_subname %} +{% endblock header_center_org_subname %} + +{% block container %} +
+
+
+
+
+
+
+
+ {% block centered_container %}{% endblock %} +
+
+
+
+
+
+
+
+{% endblock container %} + + +{% block footer_logo %} +{% endblock footer_logo %} + + +{% block footer_org_name %} + {% trans "OIDC Relying Party" %} +{% endblock footer_org_name %} + + +{% block footer_org_subname %} + {% trans "OIDC Relying Party" %} +{% endblock footer_org_subname %} + +{% block extra_scripts %} +{% block bootstrap_italia_validation %} + +{% endblock bootstrap_italia_validation %} +{% endblock extra_scripts %} + +{% block footer_menu_section %} +{% endblock footer_menu_section %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_error.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_error.html new file mode 100644 index 0000000..154a827 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_error.html @@ -0,0 +1,23 @@ + +{% extends 'rp_base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} + +
+ {% csrf_token %} +
+
+ {% trans error%} +
+ +

+ {% trans error_description%} +

+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_introspection.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_introspection.html new file mode 100644 index 0000000..e816c18 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_introspection.html @@ -0,0 +1,21 @@ +{% extends 'rp_base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} +
+ {% csrf_token %} +

{% trans 'Introspection values' %}

+ +
+ {% for attribute, value in introspection.items %} +
{{ attribute }}:
+
{{ value }}
+ {% endfor %} +
+ + Log out + Go Back +
+ +{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_landing.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_landing.html new file mode 100644 index 0000000..0997295 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/templates/rp_landing.html @@ -0,0 +1,135 @@ + +{% extends 'rp_base.html' %} +{% load i18n %} +{% load static %} + +{% block centered_container %} + +
+ +
+

+ {% trans "Welcome" %} +

+ +

+ + {% blocktrans %} + SPID is the access system that allows you to use online services with a unique digital identity. + If you already have a digital identity, login with your Identity Provider. + If you don’t have a digital identity yet, choose an Identity Provider where obtain your SPID Digital Identity. + {% endblocktrans %} + +

+ +

+ + {% blocktrans %} + If you have the new Electronic Identity Card (CIE) you can use it to access the services + online Public Administration. You need the PIN and PUK codes: + the first part of the two codes is in the receipt of the CIE request, the second part is delivered + together with the CIE. Visit the official site of CIE id to get the software, documentation and tutorial. + {% endblocktrans %} + +

+
+ + + +
+ + + +{% endblock %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/mocked_response.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/mocked_response.py new file mode 100644 index 0000000..18e9dec --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/mocked_response.py @@ -0,0 +1,212 @@ +import json +import logging + + +from spid_cie_oidc.provider.tests.settings import op_conf, op_conf_priv_jwk +from spid_cie_oidc.authority.tests.settings import rp_conf, INTERMEDIARY_JWK1 +from spid_cie_oidc.entity.jwtse import create_jws, create_jwe +from spid_cie_oidc.entity.utils import iat_now, exp_from_now, get_key, KeyUsage +from spid_cie_oidc.entity.utils import get_jwks +from cryptojwt.jws.utils import left_hash +logger = logging.getLogger(__name__) + + +class MockedTokenEndPointResponse: + def __init__(self): + self.status_code = 200 + + @property + def content(self): + id_token = { + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW', + 'at_hash': '', + 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'iss': op_conf["sub"], + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + access_token = { + 'iss': op_conf["sub"], + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"], + 'scope': 'openid', + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + id_token['at_hash'] = left_hash(jwt_at, "HS256") + jwt_id = create_jws(id_token, op_conf_priv_jwk) + + self.access_token = jwt_at + self.id_token = jwt_id + + res = { + "access_token": jwt_at, + "id_token": jwt_id, + "token_type": "Bearer", + "expires_in": 3600, + "scope": "openid", + } + return json.dumps(res).encode() + + +class MockedTokenEndPointNoCorrectResponse: + def __init__(self): + self.status_code = 200 + + @property + def content(self): + id_token = { + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW', + 'at_hash': '', + 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'iss': op_conf["sub"], + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + access_token = { + 'iss': op_conf["sub"], + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"], + 'scope': 'openid', + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + id_token['at_hash'] = left_hash(jwt_at, "HS256") + jwt_id = create_jws(id_token, op_conf_priv_jwk) + + self.access_token = jwt_at + self.id_token = jwt_id + + res = { + "access_token": jwt_at, + "token_type": "Bearer", + "expires_in": 3600, + "scope": "openid", + } + return json.dumps(res).encode() + + +class MockedTokenEndPointNoCorrectIdTokenResponse: + def __init__(self): + self.status_code = 200 + + @property + def content(self): + id_token = { + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW', + 'at_hash': '', + 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'iss': op_conf["sub"], + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + access_token = { + 'iss': op_conf["sub"], + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"], + 'scope': 'openid', + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + id_token['at_hash'] = left_hash(jwt_at, "HS256") + jwt_id = create_jws(id_token, INTERMEDIARY_JWK1) + + self.access_token = jwt_at + self.id_token = jwt_id + + res = { + "access_token": jwt_at, + "token_type": "Bearer", + "id_token": jwt_id, + "expires_in": 3600, + "scope": "openid", + } + return json.dumps(res).encode() + + +class MockedTokenEndPointNoCorrectAtHashResponse: + def __init__(self): + self.status_code = 200 + + @property + def content(self): + id_token = { + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'nonce': 'ljbvL3rpscgS4ZGda7cgibXHr7vrNREW', + 'at_hash': '45e66ce53e48273dca5a446', + 'c_hash': 'tij0h-zL_bSrsVXy-d3qHw', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'iss': op_conf["sub"], + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + access_token = { + 'iss': op_conf["sub"], + 'sub': '2ed008b45e66ce53e48273dca5a4463bc8ebd036ebaa824f4582627683c2451b', + 'aud': [rp_conf["metadata"]["openid_relying_party"]["client_id"],], + 'client_id': rp_conf["metadata"]["openid_relying_party"]["client_id"], + 'scope': 'openid', + 'jti': '402e61bd-950c-4934-83d4-c09a05468828', + 'exp': exp_from_now(), + 'iat': iat_now() + } + jwt_at = create_jws(access_token, op_conf_priv_jwk, typ="at+jwt") + jwt_id = create_jws(id_token, op_conf_priv_jwk) + + self.access_token = jwt_at + self.id_token = jwt_id + + res = { + "access_token": jwt_at, + "id_token": jwt_id, + "token_type": "Bearer", + "expires_in": 3600, + "scope": "openid", + } + return json.dumps(res).encode() + + +class MockedUserInfoResponse: + def __init__(self): + self.status_code = 200 + + @property + def content(self): + jwt = { + "sub": 'asdasdasdasasdasdas', + "https://attributes.eid.gov.it/fiscal_number": "sdfsfs908df09s8df90s8fd0" + } + jws = create_jws(jwt, op_conf_priv_jwk) + jwks = get_jwks(rp_conf["metadata"]["openid_relying_party"]) + key = get_key(jwks, KeyUsage.encryption) + jwe = create_jwe( + jws, + key + ) + return jwe.encode() + +class MockedLogout: + + @property + def content(self): + pass + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_01_rp_ops.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_01_rp_ops.py new file mode 100644 index 0000000..ce3e6ca --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_01_rp_ops.py @@ -0,0 +1,51 @@ +from django.test import Client, TestCase +from django.urls import reverse + +from spid_cie_oidc.relying_party.models import OidcAuthenticationToken +from spid_cie_oidc.relying_party.utils import html_json_preview + + +class OidcRpTest(TestCase): + # TODO: re enable + def _test_stupid_logout(self): + # a stupid logout that MUST fail + url = f'{reverse("spid_cie_rpinitiated_logout")}' + req = Client() + res = req.get(url) + self.assertTrue(res.status_code == 302) + # end stupid logout test + + # TODO: re enable + def _test_session(self): + url = f'{reverse("spid_cie_rp_begin")}?issuer_id=op_test' + req = Client() + res = req.get(url) + self.assertTrue(res.status_code == 302) + + authz_url = f'{reverse("op_test:spid_oidc_op_authz")}?{res.url.split("?")[1]}' + res = req.get(authz_url) + self.assertTrue(res.status_code == 302) + self.assertIn("code=", res.url) + + url = f'{reverse("spid_cie_rp_callback")}?{res.url.split("?")[1]}' + res = req.get(url) + self.assertTrue(res.status_code == 302) + + url = f'{reverse("spid_cie_rp_echo_attributes")}' + res = req.get(url) + self.assertIn("sando", res.content.decode()) + + # test models + tokens = OidcAuthenticationToken.objects.first() + + tokens.access_token_preview + tokens.id_token_preview + + # and now logout + url = f'{reverse("spid_cie_rpinitiated_logout")}' + res = req.get(url) + self.assertTrue(res.status_code == 302) + + # TODO: re enable + def _test_html_json_preview(self): + html_json_preview('{"a" : 34, "b" : 78}') diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_02_rp_commands.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_02_rp_commands.py new file mode 100644 index 0000000..fd8cd98 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_02_rp_commands.py @@ -0,0 +1,15 @@ +from django.test import TestCase + + +class OidcRPCliTests(TestCase): + def setUp(self): + # create a FederationEntityConfiguration SP + pass + + def test_fetch_openid_providers(self): + overrides = dict(OIDCFED_IDENTITY_PROVIDERS="", OIDCFED_REQUIRED_TRUST_MARKS="") + + with self.settings(**overrides): + pass + # out = self.call_command('fetch_openid_providers', '--start') + # self.assertEqual(out.splitlines()[0], "test arbitrary command") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_03_rp_begin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_03_rp_begin.py new file mode 100644 index 0000000..50fe41f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_03_rp_begin.py @@ -0,0 +1,198 @@ +import urllib + +from copy import deepcopy +from unittest.mock import patch + +from django.http import HttpRequest +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.entity.models import ( + FederationEntityConfiguration, + FetchedEntityStatement, + TrustChain, +) +from spid_cie_oidc.entity.jwtse import unpad_jwt_payload +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import datetime_from_timestamp, exp_from_now, iat_now +from spid_cie_oidc.provider.tests.settings import op_conf + +def create_tc(): + NOW = datetime_from_timestamp(iat_now()) + EXP = datetime_from_timestamp(exp_from_now(33)) + ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW, + ) + return TrustChain.objects.create( + sub=op_conf["sub"], + exp=EXP, + jwks = [], + status="valid", + trust_anchor=ta_fes, + is_active=True, + ) + +def create_tc_metadata_no_correct(): + NOW = datetime_from_timestamp(iat_now()) + EXP = datetime_from_timestamp(exp_from_now(33)) + ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW, + ) + local_op_conf = deepcopy(op_conf) + metadata = local_op_conf["metadata"] + metadata["openid_provider"]["jwks"] = {"keys": []} + return TrustChain.objects.create( + sub=op_conf["sub"], + exp=EXP, + status="valid", + jwks = [], + metadata=metadata, + trust_anchor=ta_fes, + is_active=True, + ) + +class RPBeginTest(TestCase): + + def setUp(self): + self.req = HttpRequest() + NOW = datetime_from_timestamp(iat_now()) + EXP = datetime_from_timestamp(exp_from_now(33)) + self.rp_conf = FederationEntityConfiguration.objects.create(**rp_conf) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW, + ) + metadata = deepcopy(op_conf["metadata"]) + self.trust_chain = TrustChain.objects.create( + sub=op_conf["sub"], + exp=EXP, + jwks = [], + metadata=metadata, + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_rp_begin(self): + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue(res.status_code == 302) + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + @patch("spid_cie_oidc.relying_party.views.rp_begin.SpidCieOidcRpBeginView.get_oidc_op", return_value=None) + def test_rp_begin_no_tc(self, mocked): + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue("request rejected" in res.content.decode()) + + def test_no_request(self): + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"request": {}}) + self.assertTrue(res.status_code == 404) + self.assertTrue("request rejected" in res.content.decode()) + self.assertTrue("Missing provider url" in res.content.decode()) + + # I changed the code to get a smarter solution + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_no_unallowed_tc(self): + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": "provider"}) + self.assertTrue(res.status_code == 403) + self.assertTrue("request rejected" in res.content.decode()) + # self.assertTrue("Unallowed Trust Anchor" in res.content.decode()) + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_no_rp_entity_conf(self): + FederationEntityConfiguration.objects.all().delete() + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue(res.status_code == 404) + self.assertTrue("request rejected" in res.content.decode()) + self.assertTrue("Missing configuration" in res.content.decode()) + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_invalid_provider_metadata(self): + metadata = deepcopy(op_conf["metadata"]) + metadata["openid_provider"].pop("jwks") + self.trust_chain.metadata = metadata + self.trust_chain.save() + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue(res.status_code == 404) + self.assertTrue("request rejected" in res.content.decode()) + self.assertTrue("Invalid provider Metadata" in res.content.decode()) + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_tc_no_active(self): + self.trust_chain.is_active = False + self.trust_chain.save() + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue(res.status_code == 404) + self.assertTrue("request rejected" in res.content.decode()) + self.assertTrue("found but DISABLED at" in res.content.decode()) + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_failed_to_get_jwks(self): + metadata = deepcopy(op_conf["metadata"]) + metadata['openid_provider'].pop("jwks") + metadata["jwks_uri"] = "jwks_uri" + self.trust_chain.metadata = metadata + self.trust_chain.save() + client = Client() + url = reverse("spid_cie_rp_begin") + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + + self.assertTrue(res.status_code == 404) + self.assertTrue("request rejected" in res.content.decode()) + + def test_rp_begin_tc_no_metadata(self): + client = Client() + url = reverse("spid_cie_rp_begin") + self.patcher = patch( + "spid_cie_oidc.relying_party.views.rp_begin.SpidCieOidcRpBeginView.get_oidc_op", + return_value = create_tc() + ) + self.patcher.start() + res = client.get(url, {"provider": op_conf["sub"], "trust_anchor": TA_SUB}) + self.assertTrue("request rejected" in res.content.decode()) + self.patcher.stop() + + @override_settings(OIDCFED_DEFAULT_TRUST_ANCHOR=TA_SUB, OIDCFED_TRUST_ANCHORS=[TA_SUB]) + def test_rp_begin_tc_no_redirect_uri(self): + red_url = "http://rp-test.it/oidc/rp/callback-test/" + FederationEntityConfiguration.objects.all().delete() + local_rp_conf = deepcopy(rp_conf) + local_rp_conf["metadata"]["openid_relying_party"]["redirect_uris"] = [red_url], + self.rp_conf = FederationEntityConfiguration.objects.create(**local_rp_conf) + client = Client() + url = reverse("spid_cie_rp_begin") + TrustChain.objects.all() + res = client.get( + url, + { + "provider": op_conf["sub"], + "trust_anchor": TA_SUB, + "redirect_uri": "http://rp-test.it/oidc/rp/callback" + } + ) + req = urllib.parse.parse_qs(res.url)['request'][0] + req_data = unpad_jwt_payload(req) + self.assertTrue(red_url in req_data['redirect_uri']) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_04_rp_callback.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_04_rp_callback.py new file mode 100644 index 0000000..8ca3d88 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_04_rp_callback.py @@ -0,0 +1,181 @@ +from copy import deepcopy +import json +from unittest.mock import patch +from django.contrib.auth import get_user_model +from django.test import Client, TestCase, override_settings +from django.urls import reverse +from spid_cie_oidc.accounts.models import User +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.utils import get_jwks + +from spid_cie_oidc.relying_party.models import OidcAuthentication +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.provider.tests.settings import op_conf +from spid_cie_oidc.provider.tests.authn_request_settings import AUTHN_REQUEST_SPID + +from spid_cie_oidc.relying_party.tests.mocked_response import ( + MockedTokenEndPointNoCorrectIdTokenResponse, + MockedTokenEndPointNoCorrectResponse, + MockedTokenEndPointResponse, + MockedTokenEndPointNoCorrectAtHashResponse, + MockedUserInfoResponse +) + + +STATE = "fyZiOL9Lf2CeKuNT2JzxiLRDink0uPcd" +CODE = "usDwMnEzJPpG5oaV8x3j&" + + +class RpCallBack(TestCase): + def setUp(self): + self.rp_jwk = { + 'use': 'sig', + 'kty': 'RSA', + 'kid': '19xSsWuFOo5bFBUECA5G3V5GEhC0s7X8TTCEykdzsmo', + 'e': 'AQAB', + 'n': 'vfzFzFAv4e1IPfH6XMeB_L3f9sRZuaOtRsAs7s6ujGv6PwVMPsdELqd2NzrmKrLih3ZysJ6RoRe87rGZUZu2GbDtZhupCPb-1MuxB810svua9PwCrQK2wADM8q26colAiAOGSt912LUPC6MOYus44xd7TUDtQcsm-3VXWu4DA19NQXOaUh2TRal6GKXr7D8teod0fo736oHTObWoDZ0KdAGlwxI6IbQKpipgTD6lZ3l9L6WSmdLI9T-TyyNV9fW6rNqhnbySCEGkCM-Up6C2GQqLTq76kQNSMGasXrd3qH07KnTLBZsFy4JV4L-ws3zgaB4PLBOdfYAYe-xSp9-H_Q', 'd': 'MYc9FXduFCrko2l6yEcmhvoE8fLcJT4bRQ-CZzswW-EmWtmJt_AAwVzcv6c2K7l4vrHNUmf0NRfYJC3ed1ztiyMJsI3TckfZxSXY39za6HIZQnaUSAAmHkXXKAjMS2Gmlg69KrW4picFZhY7AOSrbuBHP6uGbpmEbxd3D3hvBqiU6cn_TAwbIhsJniTuSYYbvHd8z93QCKBfp_nDMltz6WG8YUXFiqtzRKv62Y4K2J_zjyd7-JKiWEBh5FUNuqZ2txEV3-q6bwoh3lSGA1qZtVgU61CwHqGQK3uxfEGPV68O48NVJNEe4YgNE806IfREgprTe3osGHd2S96s-t8oYQ', + 'p': '9pwMs9MCfes5vYzXb8n1BqAL3LGdPp48jg7iKD4mZ6oLpeCbwNqtB7zN24OqrOnP2nREL2ugkSU2bBrsMy1hZ6k2DnN7cZkNgv4CZ_lO35Bc9CF9j1sCTxUnXQknympL8HVZoG2TdxUL0P09TgiBYz8SVH-uxFnlCuVZBLx35Lk', + 'q': 'xTjFHi0QGPYSXGgbUN5cl9LOw7K2ifULDbEPiYSnbzd_oVneA-q6BmanMM5CLrj8qKJPy2Cuz3do-ZZPG_SN5AU_x23K9Vga3honcomo7G0cYKi9wGPBKzAMCRNRFHp3f3BAcp6HUJRn02Q_F_xDmXeC8JiPeATSBIDCXv41I2U' + } + data = deepcopy(AUTHN_REQUEST_SPID) + self.rp_config = deepcopy(rp_conf) + authz_entry = dict( + client_id=self.rp_config["metadata"]["openid_relying_party"]["client_id"], + provider_id=op_conf["sub"], + data=json.dumps(data), + state=STATE, + provider_configuration=op_conf["metadata"]["openid_provider"], + ) + OidcAuthentication.objects.create(**authz_entry) + self.rp_config["sub"] = self.rp_config["metadata"]["openid_relying_party"]["client_id"] + FederationEntityConfiguration.objects.create(**self.rp_config) + rp_conf_saved = FederationEntityConfiguration.objects.all().first() + get_jwks(rp_conf_saved.metadata['openid_relying_party'])[0]["kid"] = rp_conf_saved.jwks_core[0]["kid"] + rp_conf_saved.save() + self.op_conf = FederationEntityConfiguration.objects.create(**op_conf) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback(self, mocked, mocked_2): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + user = get_user_model().objects.first() + self.assertTrue( + user.attributes['fiscal_number'] == "sdfsfs908df09s8df90s8fd0" + ) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_mixups_attacks(self, mocked, mocked_2): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE, "iss": "WRONG"}) + self.assertTrue(res.status_code == 400) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("spid_cie_oidc.relying_party.views.rp_callback.process_user_attributes", return_value=None) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_rp_attr_map(self, mocked, mocked_2, mocked_3): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 403) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("spid_cie_oidc.relying_party.views.rp_callback.process_user_attributes", return_value=None) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_incorret_request(self, mocked, mocked_2, mocked_3): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {}) + self.assertTrue("error" in res.json()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("spid_cie_oidc.relying_party.views.rp_callback.process_user_attributes", return_value=None) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("spid_cie_oidc.relying_party.views.rp_callback.SpidCieOidcRpCallbackView.get_userinfo", return_value=None) + def test_rp_callback_no_userinfo(self, mocked, mocked_2, mocked_3): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 400) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("spid_cie_oidc.relying_party.views.rp_callback.process_user_attributes", return_value=None) + @patch("spid_cie_oidc.relying_party.views.rp_callback.SpidCieOidcRpCallbackView.access_token_request", return_value=None) + def test_rp_callback_no_token_response(self, mocked, mocked_2): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 400) + self.assertTrue("invalid token response" in res.content.decode()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_rp_conf(self, mocked, mocked_2): + FederationEntityConfiguration.objects.filter( + sub = self.rp_config["sub"] + ).delete() + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue("Relying party not found" in res.content.decode()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_authz(self, mocked, mocked_2): + OidcAuthentication.objects.all().delete() + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue("Authentication not found" in res.content.decode()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_reunification(self, mocked, mocked_2): + user = User.objects.create(username = "username", attributes = {"fiscal_number" : "sdfsfs908df09s8df90s8fd0"}), + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + user = get_user_model().objects.first() + self.assertTrue( + user.attributes['fiscal_number'] == "sdfsfs908df09s8df90s8fd0" + ) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointNoCorrectResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_correct_token_response(self, mocked, mocked1): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 400) + self.assertTrue("invalid_request" in res.content.decode()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointNoCorrectIdTokenResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_correct_id_token_response(self, mocked, mocked1): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 403) + self.assertTrue("invalid_token" in res.content.decode()) + + @override_settings(HTTP_CLIENT_SYNC=True) + @patch("requests.post", return_value=MockedTokenEndPointNoCorrectAtHashResponse()) + @patch("requests.get", return_value=MockedUserInfoResponse()) + def test_rp_callback_no_correct_at_hash_response(self, mocked, mocked1): + client = Client() + url = reverse("spid_cie_rp_callback") + res = client.get(url, {"state": STATE, "code": CODE}) + self.assertTrue(res.status_code == 403) + self.assertTrue("at_hash verification failed" in res.content.decode()) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_05_oidc_rpinitiated_logout.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_05_oidc_rpinitiated_logout.py new file mode 100644 index 0000000..6b3f29e --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_05_oidc_rpinitiated_logout.py @@ -0,0 +1,77 @@ +from copy import deepcopy +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.provider.tests.settings import op_conf +from spid_cie_oidc.relying_party.models import ( + OidcAuthentication, + OidcAuthenticationToken +) +from spid_cie_oidc.relying_party.tests.mocked_response import MockedLogout + + +class RpIntiatedLogoutTest(TestCase): + + def setUp(self): + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + ) + self.user.set_password("test") + self.user.save() + authz_request = OidcAuthentication.objects.create( + client_id=rp_conf["metadata"]["openid_relying_party"]["client_id"], + provider_configuration=op_conf["metadata"]["openid_provider"], + ) + self.authz_token = OidcAuthenticationToken.objects.create( + user= self.user, + authz_request = authz_request, + ) + FederationEntityConfiguration.objects.create(**rp_conf) + self.admin_user = get_user_model().objects.create_superuser( + username="superuser", password="secret", email="admin@example.com" + ) + + + @patch("requests.post", return_value=MockedLogout()) + def test_rpinitiated_logout(self, mokced): + c = Client() + c.login(username="test", password="test") + url = reverse("spid_cie_rpinitiated_logout") + res = c.get(url) + self.assertTrue(res.status_code == 302) + self.assertTrue(res.url == "/oidc/rp/landing") + + def test_rpinitiated_logout_no_revocation_endpoint(self): + OidcAuthentication.objects.all().delete() + op_conf_local = deepcopy(op_conf["metadata"]["openid_provider"]) + op_conf_local.pop("revocation_endpoint") + authz_request = OidcAuthentication.objects.create( + client_id=rp_conf["metadata"]["openid_relying_party"]["client_id"], + provider_configuration=op_conf_local, + ) + self.authz_token = OidcAuthenticationToken.objects.create( + user= self.user, + authz_request = authz_request, + ) + c = Client() + c.login(username="test", password="test") + url = reverse("spid_cie_rpinitiated_logout") + res = c.get(url) + self.assertTrue(res.status_code == 302) + self.assertTrue(res.url == "/oidc/rp/landing") + + def test_rpinitiated_logout_no_auth_token(self): + OidcAuthenticationToken.objects.all().delete() + c = Client() + c.login(username="test", password="test") + url = reverse("spid_cie_rpinitiated_logout") + res = c.get(url) + self.assertTrue(res.status_code == 302) + self.assertTrue(res.url == "/oidc/rp/landing") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_06_fetch_openid_providers.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_06_fetch_openid_providers.py new file mode 100644 index 0000000..3379896 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_06_fetch_openid_providers.py @@ -0,0 +1,65 @@ + + +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase, override_settings +from spid_cie_oidc.entity.models import FetchedEntityStatement, TrustChain +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.tests.settings import op_conf + +EXP = datetime_from_timestamp(exp_from_now(33)) +NOW = datetime_from_timestamp(iat_now()) + + +def create_tc(): + ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW + ) + return TrustChain.objects.create( + sub=op_conf["sub"], + exp=EXP, + jwks = [], + metadata=op_conf["metadata"], + status="valid", + trust_anchor=ta_fes, + is_active=True, + ) + + +class FetchProviderTest(TestCase): + + def setUp(self): + + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=datetime_from_timestamp(exp_from_now(33)), + iat=datetime_from_timestamp(iat_now()), + ) + + def exec(self, cmd_name:str, *args, **kwargs): + call_command( + cmd_name, + *args, + **kwargs, + ) + + @override_settings(OIDCFED_IDENTITY_PROVIDERS = {"spid": {"http://127.0.0.1:8000/oidc/op/" :"http://testserver/"}, "cie":{}}) + def test_fetch_provider(self): + self.patcher = patch( + "spid_cie_oidc.entity.trust_chain_operations.get_or_create_trust_chain", + return_value = create_tc() + ) + self.patcher.start() + self.exec('fetch_openid_providers', '--start') + self.patcher.stop() + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_07_oidc_rp_landing.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_07_oidc_rp_landing.py new file mode 100644 index 0000000..278177b --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_07_oidc_rp_landing.py @@ -0,0 +1,40 @@ +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.entity.models import FetchedEntityStatement, TrustChain +from spid_cie_oidc.entity.tests.settings import TA_SUB +from spid_cie_oidc.entity.utils import ( + datetime_from_timestamp, + exp_from_now, + iat_now +) +from spid_cie_oidc.provider.tests.settings import op_conf + + +class RpLandingTest(TestCase): + + def setUp(self) -> None: + NOW = datetime_from_timestamp(iat_now()) + EXP = datetime_from_timestamp(exp_from_now(33)) + self.ta_fes = FetchedEntityStatement.objects.create( + sub=TA_SUB, + iss=TA_SUB, + exp=EXP, + iat=NOW, + ) + + self.trust_chain = TrustChain.objects.create( + sub=op_conf["sub"], + exp=EXP, + jwks = [], + metadata=op_conf["metadata"], + status="valid", + trust_anchor=self.ta_fes, + is_active=True, + ) + + def test_rp_landing(self): + client = Client() + url = reverse("spid_cie_rp_landing") + res = client.get(url) + self.assertTrue(res.status_code == 200) + diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_08_callback_echo_attributes.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_08_callback_echo_attributes.py new file mode 100644 index 0000000..9f78d0d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_08_callback_echo_attributes.py @@ -0,0 +1,18 @@ +from django.test import Client, TestCase +from django.urls import reverse + + +class CallBackEchoAttributesTest(TestCase): + + def setUp(self) -> None: + return super().setUp() + + def test_call_back_echo_attributes(self): + client = Client() + url = reverse("spid_cie_rp_echo_attributes") + session = client.session + session.update({"oidc_rp_user_attrs": {"email":"test@test.it"}}) + session.save() + res = client.get(url) + self.assertTrue(res.status_code == 200) + self.assertTrue("test@test.it" in res.content.decode()) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_09_rp_model.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_09_rp_model.py new file mode 100644 index 0000000..5bfaa6d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/tests/test_09_rp_model.py @@ -0,0 +1,62 @@ +import json + +from django.contrib.auth import get_user_model +from django.test import TestCase +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.provider.tests.settings import ( + op_conf, + op_conf_priv_jwk +) +from spid_cie_oidc.relying_party.models import ( + OidcAuthentication, + OidcAuthenticationToken +) + +RP_SUB = rp_conf["sub"] +RP_CLIENT_ID = rp_conf["metadata"]["openid_relying_party"]["client_id"] + +class RpModelTest(TestCase): + + def setUp(self): + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + ) + self.user.set_password("test") + self.user.save() + authz_request = OidcAuthentication.objects.create( + client_id=rp_conf["metadata"]["openid_relying_party"]["client_id"], + provider_configuration=op_conf["metadata"]["openid_provider"], + ) + self.token = { + "iss": op_conf["sub"], + "sub": RP_SUB, + "aud": [RP_CLIENT_ID], + "client_id": RP_CLIENT_ID, + "scope": "openid", + } + self.rt_jws = create_jws(self.token, op_conf_priv_jwk) + self.authz_token = OidcAuthenticationToken.objects.create( + user= self.user, + authz_request = authz_request, + code = "code", + access_token = self.rt_jws, + id_token = self.rt_jws, + ) + + def test_token_preview(self): + result = OidcAuthenticationToken.token_preview(self.authz_token, self.rt_jws) + self.assertTrue(json.loads(result)["iss"] == "http://op-test/oidc/op/") + result = OidcAuthenticationToken.token_preview(self.authz_token, "") + self.assertTrue(result == "--") + result = OidcAuthenticationToken.token_preview(self.authz_token, "token") + self.assertTrue(result == None) + result = OidcAuthenticationToken.__str__(self.authz_token) + self.assertTrue("code" in result) + result = self.authz_token.access_token_preview + self.assertTrue(json.loads(result)["iss"] == "http://op-test/oidc/op/") + result = self.authz_token.id_token_preview + self.assertTrue(json.loads(result)["iss"] == "http://op-test/oidc/op/") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/urls.py new file mode 100644 index 0000000..7af2072 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/urls.py @@ -0,0 +1,78 @@ +from django.urls import path + +from spid_cie_oidc.entity.views import ( + openid_connect_jwks_uri, + openid_connect_signed_jwks_uri +) +from .views.rp_begin import SpidCieOidcRpBeginView +from .views.rp_callback import SpidCieOidcRpCallbackView +from .views.rp_extend_session import SpidCieOidcRefreshToken +from .views.rp_callback_echo_attributes import SpidCieOidcRpCallbackEchoAttributes +from .views.rp_initiated_logout import SpidCieOidcRpLogout # oidc_rpinitiated_logout +from .views.rp_introspection import SpidCieOidcRpIntrospection +from .views.rp_landing import oidc_rp_landing +from django.contrib.auth.decorators import login_required +_PREF = "oidc/rp" + +urlpatterns = [] +urlpatterns += ( + path( + f"{_PREF}/authorization", + SpidCieOidcRpBeginView.as_view(), + name="spid_cie_rp_begin", + ), +) +urlpatterns += ( + path( + f"{_PREF}/callback", + SpidCieOidcRpCallbackView.as_view(), + name="spid_cie_rp_callback", + ), +) +urlpatterns += ( + path( + f"{_PREF}/echo_attributes", + SpidCieOidcRpCallbackEchoAttributes.as_view(), + name="spid_cie_rp_echo_attributes", + ), +) + +urlpatterns += ( + path( + f"{_PREF}/extend_session", + login_required(SpidCieOidcRefreshToken.as_view()), + name="spid_cie_rp_extend_session", + ), +) + + +urlpatterns += ( + path( + f"{_PREF}/introspection", + login_required(SpidCieOidcRpIntrospection.as_view()), + name="spid_cie_rp_echo_introspection", + ), +) + +urlpatterns += ( + path( + f"{_PREF}/logout", + login_required(SpidCieOidcRpLogout.as_view()), + name="spid_cie_rpinitiated_logout", + ), +) + +urlpatterns += ( + path(f"{_PREF}/landing", oidc_rp_landing, name="spid_cie_rp_landing"), + # path(f"{_PREF}/logout", oidc_rpinitiated_logout, name="spid_cie_rpinitiated_logout"), + path( + f"{_PREF}//jwks.json", + openid_connect_jwks_uri, + name="oidc_connect_jwks_uri", + ), + path( + f"{_PREF}//jwks.jose", + openid_connect_signed_jwks_uri, + name="oidc_connect_signed_jwks_uri", + ), +) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/utils.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/utils.py new file mode 100644 index 0000000..488eb53 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/utils.py @@ -0,0 +1,75 @@ +import base64 +import json +import hashlib +import logging +import random +import secrets +import string +import urllib + + +from django.utils.module_loading import import_string +from django.utils.safestring import mark_safe + + +logger = logging.getLogger(__name__) + + +def http_redirect_uri_to_dict(url): + splitted = urllib.parse.splitquery(url) + data = dict(urllib.parse.parse_qsl(splitted[1])) + data.update({"endpoint": splitted[0]}) + return data + + +def http_dict_to_redirect_uri_path(data): + return urllib.parse.urlencode(data) + + +def random_string(n=32): + return "".join(random.choices(string.ascii_letters + string.digits, k=n)) # nosec + + +def get_pkce(code_challenge_method: str = "S256", code_challenge_length: int = 64): + hashers = {"S256": hashlib.sha256} + + # https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + code_verifier_length = secrets.choice(range(43, 128 + 1)) + alpha = string.ascii_letters + string.digits + "-._~" + code_verifier = "".join([secrets.choice(alpha) for _ in range(code_verifier_length)]) + + code_challenge = hashers.get(code_challenge_method)( + code_verifier.encode("utf-8") + ).digest() + code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8") + code_challenge = code_challenge.replace("=", "") + + return { + "code_verifier": code_verifier, + "code_challenge": code_challenge, + "code_challenge_method": code_challenge_method, + } + + +def html_json_preview(value): + msg = json.loads(value or "{}") + dumps = json.dumps(msg, indent=2) + return mark_safe(dumps.replace("\n", "
").replace(" ", " ")) # nosec + + +def process_user_attributes(userinfo: dict, user_map: dict, authz: dict): + data = dict() + for k, v in user_map.items(): + for i in v: + if isinstance(i, str): + if i in userinfo: + data[k] = userinfo[i] + break + + elif isinstance(i, dict): + args = (userinfo, authz, i["kwargs"]) + value = import_string(i["func"])(*args) + if value: + data[k] = value + break + return data diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/__init__.py new file mode 100644 index 0000000..4fbb5b2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/__init__.py @@ -0,0 +1,171 @@ +import logging +from django.conf import settings +from pydantic import ValidationError + +from django.urls import reverse +from django.http import HttpResponseRedirect +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from ..oidc import * +from ..oauth2 import * + +from enum import Enum + +from spid_cie_oidc.entity.exceptions import InvalidTrustchain +from spid_cie_oidc.entity.models import TrustChain +from spid_cie_oidc.entity.utils import get_key +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain +from spid_cie_oidc.relying_party.exceptions import ValidationException +from spid_cie_oidc.relying_party.settings import ( + RP_DEFAULT_PROVIDER_PROFILES, + RP_PROVIDER_PROFILES +) + +logger = logging.getLogger(__name__) + + +class TokenRequestType(str, Enum): + refresh = "refresh" + revocation = "revocation" + introspection = "introspection" + + +class SpidCieOidcRp: + """ + Baseclass with common methods for RPs + """ + + def get_oidc_op(self, request) -> TrustChain: + """ + get available trust to a specific OP + """ + if not request.GET.get("provider", None): + logger.warning( + "Missing provider url. Please try '?provider=https://provider-subject/'" + ) + raise InvalidTrustchain( + "Missing provider url. Please try '?provider=https://provider-subject/'" + ) + + trust_anchor = request.GET.get("trust_anchor", None) + if trust_anchor is not None and trust_anchor not in settings.OIDCFED_TRUST_ANCHORS: + logger.warning("Unallowed Trust Anchor") + raise InvalidTrustchain("Unallowed Trust Anchor") + + if not trust_anchor: + for profile, value in settings.OIDCFED_IDENTITY_PROVIDERS.items(): + if request.GET["provider"] in value: + trust_anchor = value[request.GET["provider"]] + + if not trust_anchor: + trust_anchor = settings.OIDCFED_DEFAULT_TRUST_ANCHOR + + tc = TrustChain.objects.filter( + sub=request.GET["provider"], + trust_anchor__sub=trust_anchor, + ).first() + + discover_trust = False + if not tc: + logger.info(f'Trust Chain not found for {request.GET["provider"]}') + discover_trust = True + + elif not tc.is_active: + logger.warning(f"{tc} found but DISABLED at {tc.modified}") + raise InvalidTrustchain(f"{tc} found but DISABLED at {tc.modified}") + + elif tc.is_expired: + logger.warning(f"{tc} found but expired at {tc.exp}") + logger.warning("Try to renew the trust chain") + discover_trust = True + + if discover_trust: + tc = get_or_create_trust_chain( + subject=request.GET["provider"], + trust_anchor=trust_anchor, + # TODO - not sure that it's required for a RP that fetches OP directly from TA + # required_trust_marks = [], + force=True, + ) + return tc + + def validate_json_schema(self, request, schema_type, error_description): + try: + schema = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES] + schema[schema_type](**request) + except ValidationError as e: + logger.error( + f"{error_description} " + f"for {request.get('client_id', None)}: {e}" + ) + raise ValidationException() + + def get_token_request(self, auth_token, request, token_type): + + default_logout_url = getattr( + settings, "LOGOUT_REDIRECT_URL", None + ) or reverse("spid_cie_rp_landing") + + global audience + authz = auth_token.authz_request + + rp_conf = FederationEntityConfiguration.objects.filter( + sub=authz.client_id, + is_active=True, + ).first() + + token_request_data = dict( + client_id=auth_token.authz_request.client_id, + client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ) + + if token_type == TokenRequestType.refresh: + token_request_data["grant_type"] = "refresh_token" + token_request_data["refresh_token"] = auth_token.refresh_token + audience = authz.provider_configuration["token_endpoint"] + + elif token_type == TokenRequestType.revocation: + token_request_data["token"] = auth_token.access_token + audience = authz.provider_configuration["revocation_endpoint"] + + elif token_type == TokenRequestType.introspection: + token_request_data["token"] = auth_token.access_token + audience = authz.provider_configuration["introspection_endpoint"] + + if not audience: + logger.warning( + f"{authz.provider_id} doesn't expose the token ", token_type, " endpoint." + ) + return HttpResponseRedirect(default_logout_url) + # private_key_jwt + client_assertion = create_jws( + { + "iss": authz.client_id, + "sub": authz.client_id, + "aud": [audience], + "iat": iat_now(), + "exp": exp_from_now(), + "jti": str(uuid.uuid4()) + }, + jwk_dict = get_key(rp_conf.jwks_core) + ) + token_request_data["client_assertion"] = client_assertion + + try: + token_request = requests.post( + audience, + data=token_request_data, + timeout=getattr( + settings, "HTTPC_TIMEOUT", 8 + ) + ) # nosec - B113 + + if token_request.status_code != 200: # pragma: no cover + logger.error( + f"Something went wrong with refresh token request: {token_request.status_code}" + ) + + return token_request + + except Exception as e: # pragma: no cover + logger.error(f"Error in token request: {e}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_begin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_begin.py new file mode 100644 index 0000000..1f08fa1 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_begin.py @@ -0,0 +1,214 @@ +import json +import logging +import uuid +from copy import deepcopy + +from djagger.decorators import schema +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.utils import timezone +from django.utils.module_loading import import_string +from django.utils.translation import gettext as _ +from django.views import View +from spid_cie_oidc.entity.exceptions import InvalidTrustchain +from spid_cie_oidc.entity.jwtse import create_jws +from spid_cie_oidc.entity.utils import get_jwks, get_key, KeyUsage +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.relying_party.settings import OIDCFED_ACR_PROFILES, RP_PROVIDER_PROFILES, \ + RP_DEFAULT_PROVIDER_PROFILES + +from ..models import OidcAuthentication +from ..settings import ( + RP_PKCE_CONF, + RP_REQUEST_CLAIM_BY_PROFILE, + RP_REQUEST_EXP +) +from ..utils import ( + http_dict_to_redirect_uri_path, + random_string +) + +from . import SpidCieOidcRp + +logger = logging.getLogger(__name__) + +schema_profile = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES] + + +@schema( + summary="OIDC Relying Party Authorization begin", + methods=['GET'], + get_response_schema= { + "302": schema_profile["authorization_request_doc"], + }, + external_docs = { + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags = ['Relying Party'] +) +class SpidCieOidcRpBeginView(SpidCieOidcRp, View): + """ + View which processes the actual Authz request and + returns a Http Redirect + """ + error_template = "rp_error.html" + + def get(self, request, *args, **kwargs): + """ + http://localhost:8001/oidc/rp/authorization?provider=http://127.0.0.1:8002/&profile=spid + """ + try: + tc = self.get_oidc_op(request) + if not tc: + context = { + "error": "request rejected", + "error_description": "Trust Chain is unavailable.", + } + return render(request, self.error_template, context, status = 404) + + except InvalidTrustchain as exc: + context = { + "error": "request rejected", + "error_description": str(exc.args), + } + return render(request, self.error_template, context, status=404) + + except Exception as exc: + context = { + "error": "request rejected", + "error_description": _(str(exc.args)), + } + return render(request, self.error_template, context, status=403) + + provider_metadata = tc.metadata.get('openid_provider', None) + if not provider_metadata: + context = { + "error": "request rejected", + "error_description": _("provider metadata not found"), + } + return render(request, self.error_template, context, status=404) + + entity_conf = FederationEntityConfiguration.objects.filter( + entity_type="openid_relying_party", + # TODO: RPs multitenancy? + # sub = request.build_absolute_uri() + ).first() + if not entity_conf: + context = { + "error": "request rejected", + "error_description": _("Missing configuration."), + } + return render(request, self.error_template, context, status=404) + + client_conf = entity_conf.metadata["openid_relying_party"] + if not ( + provider_metadata.get("jwks_uri", None) + or + provider_metadata.get("jwks", None) + ): + context = { + "error": "request rejected", + "error_description": _("Invalid provider Metadata."), + } + return render(request, self.error_template, context, status=404) + + jwks_dict = get_jwks(provider_metadata, federation_jwks=tc.jwks) + + # stores the resolves jwks in the provider metadata linked to this authz request + provider_metadata['jwks'] = {'keys': jwks_dict} + if not jwks_dict: + _msg = f"Failed to get jwks from {tc.sub}" + logger.error(_msg) + context = { + "error": "request rejected", + "error_description": _msg + } + return render(request, self.error_template, context) + + authz_endpoint = provider_metadata["authorization_endpoint"] + + redirect_uri = request.GET.get("redirect_uri", client_conf["redirect_uris"][0]) + if redirect_uri not in client_conf["redirect_uris"]: + logger.warning( + f"Requested for unknown redirect uri {redirect_uri}. " + f"Reverted to default {client_conf['redirect_uris'][0]}." + ) + redirect_uri = client_conf["redirect_uris"][0] + _profile = request.GET.get("profile", "spid") + _timestamp_now = int(timezone.localtime().timestamp()) + authz_data = dict( + iss=client_conf["client_id"], + scope= request.GET.get("scope", None) or "openid", + redirect_uri=redirect_uri, + response_type=client_conf["response_types"][0], + nonce=random_string(32), + state=random_string(32), + client_id=client_conf["client_id"], + endpoint=authz_endpoint, + acr_values= OIDCFED_ACR_PROFILES, + iat=_timestamp_now, + exp =_timestamp_now + RP_REQUEST_EXP, + jti = str(uuid.uuid4()), + aud=[tc.sub, authz_endpoint], + claims=RP_REQUEST_CLAIM_BY_PROFILE[_profile], + ) + + _prompt = request.GET.get("prompt", "consent login") + + # if "offline_access" in authz_data["scope"]: + # _prompt.extend(["consent login"]) + + authz_data["prompt"] = _prompt + + # PKCE + pkce_func = import_string(RP_PKCE_CONF["function"]) + pkce_values = pkce_func(**RP_PKCE_CONF["kwargs"]) + authz_data.update(pkce_values) + # + authz_entry = dict( + client_id=client_conf["client_id"], + state=authz_data["state"], + endpoint=authz_endpoint, + # TODO: better have here an organization name + provider_id=tc.sub, + data=json.dumps(authz_data), + provider_configuration=provider_metadata, + ) + + # TODO: Prune the old or unbounded authz ... + OidcAuthentication.objects.create(**authz_entry) + + authz_data.pop("code_verifier") + # add the signed request object + authz_data_obj = deepcopy(authz_data) + authz_data_obj["iss"] = client_conf["client_id"] + + # sub claim MUST not be used to prevent that this jwt + # could be reused as a private_key_jwt + # authz_data_obj["sub"] = client_conf["client_id"] + + jwk_core_sig = get_key(entity_conf.jwks_core, KeyUsage.signature) + + request_obj = create_jws(authz_data_obj, jwk_core_sig) + authz_data["request"] = request_obj + uri_path = http_dict_to_redirect_uri_path( + { + "client_id": authz_data["client_id"], + "scope" : authz_data["scope"], + "response_type": authz_data["response_type"], + "code_challenge": authz_data["code_challenge"], + "code_challenge_method": authz_data["code_challenge_method"], + "request": authz_data["request"] + } + ) + if "?" in authz_endpoint: + qstring = "&" + else: + qstring = "?" + url = qstring.join((authz_endpoint, uri_path)) + logger.info(f"Starting Authz request to {url}") + return HttpResponseRedirect(url) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback.py new file mode 100644 index 0000000..a6fbc95 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback.py @@ -0,0 +1,317 @@ +import json +import logging + +from djagger.decorators import schema +from django.conf import settings +from django.contrib.auth import get_user_model, login +from django.http import HttpResponseRedirect, JsonResponse +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import gettext as _ +from django.views import View +from spid_cie_oidc.entity.jwtse import ( + unpad_jwt_payload, + verify_jws, + verify_at_hash +) +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.entity.settings import HTTPC_PARAMS +from spid_cie_oidc.entity.utils import get_jwks, get_jwk_from_jwt +from spid_cie_oidc.relying_party.exceptions import ValidationException +from spid_cie_oidc.relying_party.settings import RP_PROVIDER_PROFILES, RP_DEFAULT_PROVIDER_PROFILES + +from ..models import OidcAuthentication, OidcAuthenticationToken +from ..oauth2 import * +from ..oidc import * +from ..settings import ( + RP_ATTR_MAP, + RP_USER_CREATE, + RP_USER_LOOKUP_FIELD, +) +from ..utils import process_user_attributes + +from . import SpidCieOidcRp + +logger = logging.getLogger(__name__) + +schema_profile = RP_PROVIDER_PROFILES[RP_DEFAULT_PROVIDER_PROFILES] + + +@schema( + summary="OIDC Relying Party auth code Callback", + methods=['GET'], + request_schema={ + "application/x-www-form-urlencoded" : schema_profile["authn_response"], + }, + external_docs = { + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags = ['Relying Party'] +) +class SpidCieOidcRpCallbackView(View, SpidCieOidcRp, OidcUserInfo, OAuth2AuthorizationCodeGrant): + """ + View which processes an Authorization Response + https://tools.ietf.org/html/rfc6749#section-4.1.2 + + eg: + /redirect_uri?code=tYkP854StRqBVcW4Kg4sQfEN5Qz&state=R9EVqaazGsj3wg5JgxIgm8e8U4BMvf7W + """ + + error_template = "rp_error.html" + + def user_reunification(self, user_attrs: dict): + user_model = get_user_model() + + if user_attrs.get(RP_USER_LOOKUP_FIELD, None): + lookup = { + f"attributes__{RP_USER_LOOKUP_FIELD}": user_attrs.get(RP_USER_LOOKUP_FIELD) + } + else: + logger.warning("User attribute not found for reunification, try sub") + lookup = { + "username": user_attrs["username"] + } + user = user_model.objects.filter(**lookup).first() + if user: + user.attributes.update(user_attrs) + user.save() + logger.info(f"{RP_USER_LOOKUP_FIELD} matched on user {user}") + return user + elif RP_USER_CREATE: + user = user_model.objects.create( + username=user_attrs.get("username", user_attrs["sub"]), + first_name=user_attrs.get("given_name", user_attrs["sub"]), + last_name=user_attrs.get("family_name", user_attrs["sub"]), + email=user_attrs.get("email", ""), + attributes=user_attrs, + ) + logger.info(f"Created new user {user}") + return user + + def get(self, request, *args, **kwargs): + """ + The Authorization callback, the redirect uri where the auth code lands + """ + request_args = {k: v for k, v in request.GET.items()} + if "error" in request_args: + return render( + request, + self.error_template, + request_args, + status=401 + ) + authz = OidcAuthentication.objects.filter( + state=request_args.get("state"), + ) + try: + self.validate_json_schema( + request.GET.dict(), + "authn_response", + "Authn response object validation failed" + ) + except ValidationException: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Authn response object validation failed", + }, + status = 400 + ) + + if not authz: + context = { + "error": "unauthorized request", + "error_description": _("Authentication not found"), + } + return render(request, self.error_template, context, status=401) + else: + authz = authz.last() + + code = request.GET.get("code") + # mixups attacks prevention + if request.GET.get('iss', None): + if request.GET['iss'] != authz.provider_id: + context = { + "error": "invalid request", + "error_description": _( + "authn response validation failed: mixups attack prevention." + ), + } + return render(request, self.error_template, context, status=400) + + authz_token = OidcAuthenticationToken.objects.create( + authz_request=authz, code=code + ) + self.rp_conf = FederationEntityConfiguration.objects.filter( + sub=authz_token.authz_request.client_id + ).first() + if not self.rp_conf: + context = { + "error": "invalid request", + "error_description": _("Relying party not found"), + } + return render(request, self.error_template, context, status=400) + + authz_data = json.loads(authz.data) + token_response = self.access_token_request( + redirect_uri=authz_data["redirect_uri"], + state=authz.state, + code=code, + issuer_id=authz.provider_id, + client_conf=self.rp_conf, + token_endpoint_url=authz.provider_configuration["token_endpoint"], + audience=[authz.provider_id], + code_verifier=authz_data.get("code_verifier"), + ) + if not token_response: + context = { + "error": "invalid token response", + "error_description": _("Token response seems not to be valid"), + } + return render(request, self.error_template, context, status=400) + + else: + try: + self.validate_json_schema( + token_response, + "token_response", + "Token response object validation failed" + ) + except ValidationException: + return JsonResponse( + { + "error": "invalid_request", + "error_description": "Token response object validation failed", + }, + status = 400 + ) + jwks = get_jwks(authz.provider_configuration) + access_token = token_response["access_token"] + id_token = token_response["id_token"] + + op_ac_jwk = get_jwk_from_jwt(access_token, jwks) + op_id_jwk = get_jwk_from_jwt(id_token, jwks) + + if not op_ac_jwk or not op_id_jwk: + logger.warning( + "Token signature validation error, " + f"the tokens were signed with a different kid from: {jwks}." + ) + context = { + "error": "invalid_token", + "error_description": _("Authentication token seems not to be valid."), + } + return render(request, self.error_template, context, status=403) + + try: + verify_jws(access_token, op_ac_jwk) + except Exception as e: + logger.warning( + f"Access Token signature validation error: {e} " + ) + context = { + "error": "token verification failed", + "error_description": _("Authentication token validation error."), + } + return render(request, self.error_template, context, status=403) + + try: + verify_jws(id_token, op_id_jwk) + except Exception as e: + logger.warning( + f"ID Token signature validation error: {e} " + ) + context = { + "error": "token verification failed", + "error_description": _("ID token validation error."), + } + return render(request, self.error_template, context, status=403) + + decoded_id_token = unpad_jwt_payload(id_token) + logger.debug(decoded_id_token) + + try: + verify_at_hash(decoded_id_token, access_token) + except Exception as e: + logger.warning( + f"at_hash validation error: {e} " + ) + context = { + "error": "at_hash verification failed", + "error_description": _("at_hash validation error."), + } + return render(request, self.error_template, context, status=403) + + decoded_access_token = unpad_jwt_payload(access_token) + logger.debug(decoded_access_token) + + authz_token.access_token = access_token + authz_token.id_token = id_token + authz_token.scope = token_response.get("scope") + authz_token.token_type = token_response["token_type"] + authz_token.expires_in = token_response["expires_in"] + authz_token.save() + + userinfo = self.get_userinfo( + authz.state, + authz_token.access_token, + authz.provider_configuration, + verify=HTTPC_PARAMS.get("connection", {}).get("ssl", True) + ) + if not userinfo: + logger.warning( + "Userinfo request failed for state: " + f"{authz.state} to {authz.provider_id}" + ) + context = { + "error": "invalid userinfo response", + "error_description": _("UserInfo response seems not to be valid"), + } + return render(request, self.error_template, context, status=400) + + # here django user attr mapping + user_attrs = process_user_attributes(userinfo, RP_ATTR_MAP, authz.__dict__) + if not user_attrs: + _msg = "No user attributes have been processed" + logger.warning(f"{_msg}: {userinfo}") + # TODO: verify error message and status + context = { + "error": "missing user attributes", + "error_description": _(f"{_msg}: {userinfo}"), + } + return render(request, self.error_template, context, status=403) + + user = self.user_reunification(user_attrs) + if not user: + # TODO: verify error message and status + context = {"error": _("No user found"), "error_description": _("")} + return render(request, self.error_template, context, status=403) + + request.session["rt_expiration"] = 0 + + if token_response.get('refresh_token', None): + refresh_token = token_response["refresh_token"] + authz_token.refresh_token = refresh_token + decoded_refresh_token = unpad_jwt_payload(refresh_token) + request.session["rt_expiration"] = decoded_refresh_token['exp'] - iat_now() + request.session["rt_jti"] = decoded_refresh_token['jti'] + logger.info(decoded_refresh_token) + + # authenticate the user + login(request, user) + request.session["oidc_rp_user_attrs"] = user_attrs + + request.session["at_expiration"] = decoded_access_token['exp'] - iat_now() + request.session["at_jti"] = decoded_access_token['jti'] + + authz_token.user = user + authz_token.save() + return HttpResponseRedirect( + getattr( + settings, "LOGIN_REDIRECT_URL", None + ) or reverse("spid_cie_rp_echo_attributes") + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback_echo_attributes.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback_echo_attributes.py new file mode 100644 index 0000000..5c6ad52 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_callback_echo_attributes.py @@ -0,0 +1,19 @@ +import logging + +from django.shortcuts import render +from django.views import View + + +logger = logging.getLogger(__name__) + + +class SpidCieOidcRpCallbackEchoAttributes(View): + template = "echo_attributes.html" + + def get(self, request): + data = {"oidc_rp_user_attrs": request.session.get("oidc_rp_user_attrs", {}), + "at_expiration": request.session.get("at_expiration"), + "at_jti": request.session.get("at_jti"), + "rt_expiration": request.session.get("rt_expiration"), + "rt_jti": request.session.get("rt_jti")} + return render(request, self.template, data) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_extend_session.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_extend_session.py new file mode 100644 index 0000000..fe06c1f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_extend_session.py @@ -0,0 +1,78 @@ +import logging + +from djagger.decorators import schema +from django.http import HttpResponseRedirect +from django.urls import reverse + +from ..models import OidcAuthenticationToken +from ..oauth2 import * +from ..oidc import * + +from . import SpidCieOidcRp, TokenRequestType +from django.views import View + +from spid_cie_oidc.entity.jwtse import ( + unpad_jwt_payload, +) + +logger = logging.getLogger(__name__) + + +@schema( + summary="OIDC Relying party refresh token request", + methods=['GET'], + external_docs={ + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags=['Relying Party'] +) +class SpidCieOidcRefreshToken(SpidCieOidcRp, View): + error_template = "rp_error.html" + + def get(self, request, *args, **kwargs): + """ + Call the token endpoint of the op + """ + auth_tokens = OidcAuthenticationToken.objects.filter( + user=request.user + ).filter(revoked__isnull=True) + + if not auth_tokens: + logger.warning( + "Token request failed: not found any authentication session" + ) + + auth_token = auth_tokens.last() + + try: + token_response = self.get_token_request(auth_token, request, TokenRequestType.refresh) # "refresh") + if token_response.status_code == 400: + return HttpResponseRedirect(reverse("spid_cie_rp_landing")) + + refresh_token_response = json.loads(token_response.content.decode()) + + auth_token.refresh_token = refresh_token_response["refresh_token"] + auth_token.access_token = refresh_token_response["access_token"] + auth_token.save() + + decoded_access_token = unpad_jwt_payload(refresh_token_response["access_token"]) + decoded_refresh_token = unpad_jwt_payload(refresh_token_response["refresh_token"]) + + request.session["rt_expiration"] = decoded_refresh_token['exp'] - iat_now() + request.session["rt_jti"] = decoded_refresh_token['jti'] + request.session["oidc_rp_user_attrs"] = request.user.attributes + + request.session["at_expiration"] = decoded_access_token['exp'] - iat_now() + request.session["at_jti"] = decoded_access_token['jti'] + + return HttpResponseRedirect( + getattr( + settings, "LOGIN_REDIRECT_URL", None + ) or reverse("spid_cie_rp_echo_attributes") + ) + except Exception as e: # pragma: no cover + logger.warning(f"Refresh Token request failed: {e}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_initiated_logout.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_initiated_logout.py new file mode 100644 index 0000000..c323d98 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_initiated_logout.py @@ -0,0 +1,60 @@ +import logging + + +from djagger.decorators import schema +from django.conf import settings +from django.contrib.auth import logout +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.utils import timezone +from ..models import OidcAuthenticationToken + +from . import SpidCieOidcRp, TokenRequestType +from django.views import View + +logger = logging.getLogger(__name__) + + +@schema( + summary="OIDC Relying party logout", + methods=['GET'], + external_docs={ + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags=['Relying Party'] +) +class SpidCieOidcRpLogout(SpidCieOidcRp, View): + def get(self, request): + """ + Call the token revocation endpoint of the op + """ + auth_tokens = OidcAuthenticationToken.objects.filter( + user=request.user + ).filter(revoked__isnull=True) + + default_logout_url = getattr( + settings, "LOGOUT_REDIRECT_URL", None + ) or reverse("spid_cie_rp_landing") + if not auth_tokens: + logger.warning( + "Token revocation failed: not found any authentication session" + ) + return HttpResponseRedirect(default_logout_url) + + auth_token = auth_tokens.last() + logger.info(f"{request.user} logout") + logout(request) + + try: + self.get_token_request(auth_token, request, TokenRequestType.revocation) # "revocation") + auth_token.logged_out = timezone.localtime() + auth_token.save() + except Exception as e: # pragma: no cover + logger.warning(f"Token revocation failed: {e}") + + auth_tokens.update(revoked=timezone.localtime()) + return HttpResponseRedirect(default_logout_url) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_introspection.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_introspection.py new file mode 100644 index 0000000..f4a954f --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_introspection.py @@ -0,0 +1,56 @@ +import logging + +from djagger.decorators import schema + +from ..models import OidcAuthenticationToken +from ..oauth2 import * +from ..oidc import * + +from . import SpidCieOidcRp, TokenRequestType +from django.views import View + + +from django.shortcuts import render + +logger = logging.getLogger(__name__) + + +@schema( + summary="OIDC Relying party refresh token request", + methods=['GET'], + external_docs={ + "alt_text": "AgID SPID OIDC Guidelines", + "url": ( + "https://www.agid.gov.it/it/agenzia/stampa-e-comunicazione/" + "notizie/2021/12/06/openid-connect-spid-adottate-linee-guida" + ), + }, + tags=['Relying Party'] +) +class SpidCieOidcRpIntrospection(SpidCieOidcRp, View): + error_template = "rp_error.html" + template = "rp_introspection.html" + + def get(self, request, *args, **kwargs): + """ + Call the token endpoint of the op + """ + auth_tokens = OidcAuthenticationToken.objects.filter( + user=request.user + ).filter(revoked__isnull=True) + + if not auth_tokens: + logger.warning( + "Token request failed: not found any authentication session" + ) + + auth_token = auth_tokens.last() + + try: + token_response = self.get_token_request(auth_token, request, TokenRequestType.introspection) + introspection_token_response = json.loads(token_response.content.decode()) + data = {"introspection": introspection_token_response} + return render(request, self.template, data) + + except Exception as e: # pragma: no cover + logger.warning(f"Refresh Token request failed: {e}") diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_landing.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_landing.py new file mode 100644 index 0000000..8141f0a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/views/rp_landing.py @@ -0,0 +1,60 @@ +from copy import deepcopy +import logging +import random + + +from django.conf import settings +from django.shortcuts import render +from spid_cie_oidc.entity.trust_chain_operations import get_or_create_trust_chain + +logger = logging.getLogger(__name__) + + +def oidc_rp_landing(request): + spid_providers = { + k: {"sub": v} for k, v in + settings.OIDCFED_IDENTITY_PROVIDERS.get("spid", {}).items() + } + cie_providers = { + k: {"sub": v} for k, v in + settings.OIDCFED_IDENTITY_PROVIDERS.get("cie", {}).items() + } + + providers = deepcopy(spid_providers) + providers.update(cie_providers) + subs = list(spid_providers.keys()) + list(cie_providers.keys()) + + tcs = [] + for sub in subs: + try: + tc = get_or_create_trust_chain( + subject = sub, + trust_anchor = providers[sub]["sub"] + ) + tcs.append(tc) + except Exception as e: + logger.warning( + f"Failed trust chain for {sub} to {providers[sub]}: {e}" + ) + + for i in tcs: + if i.sub in settings.OIDCFED_IDENTITY_PROVIDERS.get("spid", {}): + target = spid_providers + elif i.sub in settings.OIDCFED_IDENTITY_PROVIDERS.get("cie", {}): + target = cie_providers + else: + continue + + target[i.sub] = { + "sub": i.sub, + "logo_uri" : i.metadata.get("openid_provider", {}).get("logo_uri", ""), + "organization_name": i.metadata.get("openid_provider", {}).get("organization_name","") + } + + s_spid_providers = list(spid_providers.items()) + random.shuffle(s_spid_providers) + content = { + "spid_providers": dict(s_spid_providers), + "cie_providers" : cie_providers + } + return render(request, "rp_landing.html", content) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/admin.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/admin.py new file mode 100644 index 0000000..d9a54e2 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/admin.py @@ -0,0 +1,28 @@ +from django.contrib import admin + +from .models import RelyingPartyReport, RelyingPartyTest + + +@admin.register(RelyingPartyReport) +class RelyingPartyReportAdmin(admin.ModelAdmin): + search_fields = ("client_id",) + list_display = ( + "client_id", "status", "user", "created", "modified" + ) + list_filter = ("created", "status") + + +@admin.register(RelyingPartyTest) +class RelyingPartyTestAdmin(admin.ModelAdmin): + search_fields = ("name", "report__client_id") + list_display = ( + "client_id", "status", + "code", "http_status_code", "screenshot_as_html" + ) + list_filter = ("created", "status") + readonly_fileds = ("screenshot_as_html",) + + def screenshot_as_html(self, obj): + return obj.screenshot # pragma: no cover + + screenshot_as_html.short_description = "Screenshot" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/apps.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/apps.py new file mode 100644 index 0000000..6cd6e44 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class RelyingPartyTestConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "spid_cie_oidc.relying_party_test" + verbose_name = "SPID/CIE OIDC Federation RP Tests" + label = "spid_cie_oidc_relying_party_test" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/forms.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/forms.py new file mode 100644 index 0000000..e3e9844 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/forms.py @@ -0,0 +1,28 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class TestingPageAttributesForm(forms.Form): + + attributes = forms.JSONField( + initial=dict, + label="attributes" + ) + + +class TestingPageChecksForm(forms.Form): + + CHOICES = ( + ('send_auth_code_no_code', _('[auth code] send an auth code response without code')), + ('send_auth_code_no_state', _('[auth code] send an auth code without state')), + ('unsigned_id_token', _('[token endpoint] release an unsigned id token')), + ('id_token_with_wrong_signature', _('[token endpoint] release an id token with a wrong signature')) + ) + + test = forms.ChoiceField( + choices=CHOICES, + widget=forms.RadioSelect, + label="select", + # error_messages={"required": _("Select an item")}, + required=False + ) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0001_initial.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0001_initial.py new file mode 100644 index 0000000..007818a --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 4.0.2 on 2022-03-13 00:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import spid_cie_oidc.relying_party_test.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='RelyingPartyReport', + fields=[ + ('id', models.BigAutoField(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.URLField(blank=True, null=True)), + ('status', models.CharField(choices=[('unknown', 'Sconosciuto'), ('failed', 'Failed'), ('successfull', 'successfull'), ('error', 'Internal error')], default='unknown', help_text='status', max_length=33)), + ('report', models.JSONField(blank=True, default=dict, max_length=4096)), + ('user', models.ForeignKey(blank=True, help_text='Staff user that started the tests', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Relying Party Status', + 'verbose_name_plural': 'Relying Party Status', + }, + ), + migrations.CreateModel( + name='RelyingPartyTest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=256)), + ('category', models.CharField(max_length=256)), + ('code', models.CharField(max_length=256)), + ('http_status_code', models.PositiveIntegerField()), + ('screenshot', models.FileField(upload_to=spid_cie_oidc.relying_party_test.models.context_media_path)), + ('log', models.TextField()), + ('note', models.TextField()), + ('status', models.CharField(choices=[('unknown', 'Sconosciuto'), ('failed', 'Failed'), ('successfull', 'successfull'), ('error', 'Internal error')], default='unknown', help_text='status', max_length=33)), + ('report', models.ForeignKey(help_text='Staff user that started the tests', on_delete=django.db.models.deletion.CASCADE, to='spid_cie_oidc_relying_party_test.relyingpartyreport')), + ], + options={ + 'verbose_name': 'Relying Party Test', + 'verbose_name_plural': 'Relying Party Tests', + }, + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0002_alter_relyingpartytest_report.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0002_alter_relyingpartytest_report.py new file mode 100644 index 0000000..7d67ba4 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/0002_alter_relyingpartytest_report.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.2 on 2022-03-14 16:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('spid_cie_oidc_relying_party_test', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='relyingpartytest', + name='report', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spid_cie_oidc_relying_party_test.relyingpartyreport'), + ), + ] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/models.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/models.py new file mode 100644 index 0000000..5363aae --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/models.py @@ -0,0 +1,93 @@ +import logging + +from django.contrib.auth import get_user_model +from django.db import models +from django.utils.translation import gettext as _ +from spid_cie_oidc.entity.abstract_models import TimeStampedModel + + +logger = logging.getLogger(__name__) + + +TEST_STATUS = ( + ("unknown", _("Unknown")), + ("failed", _("Failed")), + ("successfull", _("successfull")), + ("error", _("Internal error")) +) + + +def context_media_path(instance, filename): # pragma: no cover + # file will be uploaded to MEDIA_ROOT/... + return ( + f'medias/{instance.client_id}/' + f'{instance.report.pk}/' + f'{filename}' + ) + + +class RelyingPartyReport(TimeStampedModel): + """ + A test report about a RP + """ + + client_id = models.URLField(blank=True, null=True) + user = models.ForeignKey( + get_user_model(), on_delete=models.SET_NULL, + null=True, blank=True, + help_text=_("Staff user that started the tests") + ) + status = models.CharField( + max_length=33, + default = "unknown", + help_text=_("status"), + choices = TEST_STATUS + ) + + report = models.JSONField(max_length=4096, blank=True, default=dict) + + def __str__(self): + return "{} {}".format(self.client_id, self.status) + + class Meta: + verbose_name = "Relying Party Status" + verbose_name_plural = "Relying Party Status" + + +class RelyingPartyTest(TimeStampedModel): + """ + Single tests that belongs to a Report + """ + + report = models.ForeignKey( + RelyingPartyReport, on_delete=models.CASCADE, + help_text=_("") + ) + name = models.CharField(max_length=256) + category = models.CharField(max_length=256) + code = models.CharField(max_length=256) + http_status_code = models.PositiveIntegerField() + + screenshot = models.FileField( + upload_to=context_media_path + ) + log = models.TextField() + note = models.TextField() + + status = models.CharField( + max_length=33, + default = "unknown", + help_text=_("status"), + choices = TEST_STATUS + ) + + @property + def client_id(self): + return self.report.client_id + + def __str__(self): + return "{} {}".format(self.client_id, self.status) + + class Meta: + verbose_name = "Relying Party Test" + verbose_name_plural = "Relying Party Tests" diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/README.md b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/README.md new file mode 100644 index 0000000..ec7c23d --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/README.md @@ -0,0 +1,23 @@ +# Pyppeteer + +Note: When you run pyppeteer for the first time, it downloads the latest version of Chromium (~150MB) if it is not found on your system. If you don't prefer this behavior, ensure that a suitable Chrome binary is installed. One way to do this is to run pyppeteer-install command before prior to using this library. + +- Documentation: https://pyppeteer.github.io/pyppeteer/ + +## Requisition: + +pyppeteer requires Python >= 3.6 + +## Installation: + +Install with pip from PyPI: + +``` +pip install pyppeteer +``` + +Or install the latest version from [this github repo](https://github.com/pyppeteer/pyppeteer/): + +``` +pip install -U git+https://github.com/pyppeteer/pyppeteer@dev +``` diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/crawler.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/crawler.py new file mode 100644 index 0000000..ce9d442 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/crawler.py @@ -0,0 +1,86 @@ +import asyncio +from pyppeteer import launch +import os +import json + +BASE_FOLDER = os.path.join(".","spid_cie_oidc","relying_party_test","snippets","results") + + +class Crawler: + + def __init__(self, url: str, username: str, password: str, testname: str, browser=None): + self.url = url + self.username = username + self.password = password + self.testname = testname + self.waiting = {"timeout": 60000, "waitUntil": "networkidle0"} + if browser: + self.browser = browser + + def make_dir_result(self): + self.folder = os.path.join(BASE_FOLDER, self.testname) + if not os.path.exists(self.folder): + os.mkdir(self.folder) + + async def start(self, page= None): + self.browser = await launch(headless=False, args= ['--start-maximized']) + if page: + self.page = page + else: + self.page = await self.browser.newPage() + await self.page.setViewport({"width": 1866, "height": 900}) + + page = await self.page.goto(self.url) + self.status = page.status + + # async def init_browser(self): + # self.browser = await launch(headless=False, args= [ '--start-maximized' ]) + + # async def make_page(self,page=None): + # if page: + # self.page = page + # else: + # self.page = await self.browser.newPage() + # await self.page.setViewport({ "width": 1866, "height": 900}) + + # async def goto_url(self): + # await self.page.goto(self.url) + + async def goto_login_provider(self): + spid_button = await self.page.querySelector(".italia-it-button-icon") + await spid_button.click() + provider = await self.page.querySelector("#provider-spid") + await provider.click() + await asyncio.ensure_future(self.page.waitForNavigation(self.waiting)) + + async def login(self): + lable1 = await self.page.querySelector("#label_username") + await lable1.click() + await lable1.type(self.username) + lable2 = await self.page.querySelector("#label_password") + await lable2.click() + await lable2.type(self.password) + submit = await self.page.querySelector("#submit") + await submit.click() + page = await asyncio.ensure_future(self.page.waitForNavigation(self.waiting)) + self.status = page.status + + async def consent(self, selector:str): + button = await self.page.querySelector(selector) + await button.click() + page = await asyncio.ensure_future(self.page.waitForNavigation(self.waiting)) + self.status = page.status + + async def print_result(self, content, filename, extension): + file_name = os.path.join(self.folder, f'{filename}{extension}') + f = open(file_name, 'w') + f.write(content) + f.close() + + async def save_result(self): + file_path = os.path.join(self.folder, f'{self.testname}.png') + html = await self.page.content() + status_code = {"status_code": self.status} + await self.print_result(html, self.testname, ".html") + await self.print_result(json.dumps(status_code), "status", ".json") + await self.page.screenshot({'path': file_path}) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/pyppeteer_tests.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/pyppeteer_tests.py new file mode 100644 index 0000000..21e06d0 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/snippets/pyppeteer_tests.py @@ -0,0 +1,40 @@ +import asyncio +from crawler import Crawler + +OIDC_RP_REQUESTS = { + "full_authentication_ok": "http://localhost:8000/oidc/rp/landing", +} + + +class Pypper: + + def __init__(self) -> None: + pass + + async def authentication_ok(): + c = Crawler(OIDC_RP_REQUESTS["full_authentication_ok"], + "user", + "oidcuser", + "authentication_ok" + ) + c.make_dir_result() + await c.start() + await c.goto_login_provider() + await c.login() + await c.consent("#agree") + await c.save_result() + await c.browser.close() + + async def test2(): + c = Crawler(OIDC_RP_REQUESTS["request_ok"], + "user", + "oidcuser", + "send_request_ok" + ) + c.make_dir_result() + await c.start() + await c.save_result() + await c.browser.close() + + if __name__ == "__main__": + asyncio.run(authentication_ok()) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/static/images/oops.webp b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/static/images/oops.webp new file mode 100644 index 0000000000000000000000000000000000000000..3e898d9b1128674fbc9d72400d46744e8f594a2b GIT binary patch literal 11982 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 I?!UnQ15%6Sk^lez literal 0 HcmV?d00001 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/templates/op_user_staff_test.html b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/templates/op_user_staff_test.html new file mode 100644 index 0000000..4bafcaa --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/templates/op_user_staff_test.html @@ -0,0 +1,219 @@ + +{% extends 'op_base.html' %} +{% load i18n %} +{% load static %} + +{% block menu_links %} + + + + +{% endblock menu_links %} +{% block container %} +
+
+ + +
+
+ +
+ {% csrf_token %} + +
+
+
+
+
+

{% trans 'Relying Party test page' %}

+

{% trans "You're a staff user and currently testing " %}{{session.client_id}}.

+
+
+
+
+
+
+ + +
+
+
+
+
+
+ + + +
+
  + + Configuration +
+
+ +
+ +
+ +
+
+
+

{% trans "The callback url where the auth code will be sent" %}

+ + +
+
+
+ +
+
+
+ + + +
+
+
+ +
+
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
  + + Tests
+
+
+ +
+ +
+
+
+ +
+
+
    +
  • {% trans "automatic test" %}
  • +
  • response code: 200
  • +
  • authz url: url
  • +
  • redirect uri: url
  • +
  • last : datetime here
  • +
+ +
+
+ +
+
+
+ +
+ + +
+ + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ + + {% trans "Deny consent" %} + + + + +
+ +
+
+
+
+ + + +
+ +
+
+
+ +{% endblock container %} \ No newline at end of file diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/__init__.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/test_01_user_staff.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/test_01_user_staff.py new file mode 100644 index 0000000..eba43b8 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/tests/test_01_user_staff.py @@ -0,0 +1,72 @@ +from django.contrib.auth import get_user_model +from django.test import Client, TestCase +from django.urls import reverse +from spid_cie_oidc.authority.tests.settings import rp_conf +from spid_cie_oidc.entity.models import FederationEntityConfiguration +from spid_cie_oidc.provider.models import OidcSession +from spid_cie_oidc.provider.tests.settings import op_conf + + +class RPTetstTest(TestCase): + + def setUp(self): + self.user = get_user_model().objects.create( + username="test", + first_name="test", + last_name="test", + email="test@test.it", + is_staff = True + ) + self.user.set_password("test") + self.user.save() + OidcSession.objects.create( + user=self.user, + user_uid="", + nonce="", + authz_request={ + "scope": "openid", + "nonce": "123", + "acr_values" : ["https://www.spid.gov.it/SpidL2"], + "redirect_uri" : rp_conf["metadata"]["openid_relying_party"]["redirect_uris"][0], + "state" : "state", + }, + client_id="", + auth_code="code", + ) + self.op_conf = FederationEntityConfiguration.objects.create(**op_conf) + + + def test_user_staff_get(self): + c = Client() + c.login(username="test", password="test") + session = c.session + session.update({"oidc": {"auth_code": "code"}}) + session.save() + url = reverse("oidc_provider_staff_testing") + res = c.get(url) + self.assertTrue("test page" in res.content.decode()) + + def test_user_staff_post(self): + c = Client() + c.login(username="test", password="test") + session = c.session + session.update({"oidc": {"auth_code": "code"}}) + session.save() + url = reverse("oidc_provider_staff_testing") + res = c.post(url) + self.assertTrue(res.status_code == 302) + self.assertTrue("callback" in res.url) + + def test_user_staff_get_no_session(self): + c = Client() + c.login(username="test", password="test") + url = reverse("oidc_provider_staff_testing") + res = c.get(url) + self.assertTrue(res.status_code == 403) + + def test_user_staff_post_no_session(self): + c = Client() + c.login(username="test", password="test") + url = reverse("oidc_provider_staff_testing") + res = c.post(url) + self.assertTrue(res.status_code == 403) diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/urls.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/urls.py new file mode 100644 index 0000000..11463c7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/urls.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.urls import path + +from .views import ( + StaffTestingPageView +) + +_PREF = getattr(settings, "OIDC_PREFIX", "oidc/op/rp-test/") + +urlpatterns = [ + + path( + f"{_PREF}landing/", + StaffTestingPageView.as_view(), + name="oidc_provider_staff_testing", + ), + +] diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/views.py b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/views.py new file mode 100644 index 0000000..1dcf8a7 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/views.py @@ -0,0 +1,73 @@ +import json +import logging + +from django.http import HttpResponseForbidden +from django.views import View +from django.shortcuts import render +from spid_cie_oidc.provider.models import IssuedToken +from spid_cie_oidc.provider.views import OpBase +from spid_cie_oidc.relying_party_test.forms import TestingPageAttributesForm, TestingPageChecksForm, _ + + +logger = logging.getLogger(__name__) + + +class StaffTestingPageView(OpBase, View): + + template = "op_user_staff_test.html" + + def get_testing_form(self): + return TestingPageChecksForm + + def get(self, request): + try: + session = self.check_session(request) + except Exception: + logger.warning("Invalid session") + return HttpResponseForbidden() + + session.user + attributes = self.attributes_names_to_release( + request, session + )['filtered_user_claims'] + + content = { + "form_checks": self.get_testing_form()(), + "form_attrs": TestingPageAttributesForm(), + "attributes": json.dumps(attributes, indent=4), + "session": session, + "redirect_uri": session.authz_request.get('redirect_uri', "") + } + return render(request, self.template, content) + + def post(self, request): + + try: + session = self.check_session(request) + except Exception: + logger.warning("Invalid session") + return HttpResponseForbidden() + + form = self.get_testing_form()(request.POST) + if not form.is_valid(): + return self.redirect_response_data( + # TODO: this is not normative -> check AgID/IPZS + session.authz_request['redirect_uri'], + error="rejected_by_user", + error_description=_( + "User rejected the release of attributes" + ) + ) + + issuer = self.get_issuer() + iss_token_data = self.get_iss_token_data(session, issuer) + + IssuedToken.objects.create(**iss_token_data) + self.payload = session.authz_request + + return self.redirect_response_data( + self.payload["redirect_uri"], + code=session.auth_code, + state=session.authz_request["state"], + iss=issuer.sub if issuer else "" + )