diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..3499e76 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,12 @@ +# .coveragerc to control coverage.py +[coverage:run] +branch = True +source = src/djangosaml2_spid/ +omit = + *manage.py + */distutils/* + tests/* + */apps/* + */migrations/* + */site-packages/* + */tests.py diff --git a/.gitignore b/.gitignore index 46658d8..987eb1a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ __pycache__/ *.py[cod] *$py.class - - # C extensions *.so @@ -129,3 +127,7 @@ dmypy.json # Pyre type checker .pyre/ + +# example project downloaded metadata +example/spid_config/metadata/idp_*.xml +example/djangosaml2_spid diff --git a/.gitmodules b/.gitmodules index 98c8352..c15b06c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ +[submodule "spid-compliant-certificates"] + path = spid-compliant-certificates + url = https://github.com/italia/spid-compliant-certificates.git [submodule "spid-saml-check"] path = spid-saml-check - url = ./spid-saml-check + url = https://github.com/italia/spid-saml-check.git diff --git a/README.md b/README.md index e485128..e4b711c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Djangosaml2 SPID -================ +SPID Django ![CI build](https://github.com/italia/spid-django/workflows/spid-django/badge.svg) ![Python version](https://img.shields.io/badge/license-Apache%202-blue.svg) @@ -15,7 +14,7 @@ Introduction This is a Django application that provides a SAML2 Service Provider for a Single Sign On with SPID, the Italian Digital Identity System. -This project comes with a demo on a Spid button template with both *spid-testenv2* +This project comes with a demo on a Spid button template with both *spid-testenv2* and *spid-saml-check* IDP preconfigured. See running the Demo project paragaph for details. The technical documentation on SPID and SAML is available at [Docs Italia](https://docs.italia.it/italia/spid/spid-regole-tecniche/it/34.1.1/index.html) @@ -26,7 +25,7 @@ The technical documentation on SPID and SAML is available at [Docs Italia](https Dependencies ------------ -These libraries are required on your operating system environment +These libraries are required on your operating system environment in order to compile external modules of some dependencies: - xmlsec @@ -39,7 +38,7 @@ in order to compile external modules of some dependencies: Running the Demo project ------------------------ -The demo project is configured within `example/` subdirectory. +The demo project is configured within `example/` subdirectory. This project uses **spid-saml-check** and **spid-testenv2** as additional IDPs configured in a demo SPID button. @@ -51,20 +50,20 @@ source env/bin/activate pip install -r ../requirements.txt ```` -Your example saml2 configuration is in `spid_config/spid_settings.py`. +Your example saml2 configuration is in `spid_config/spid_settings.py`. See djangosaml2 or pysaml2 official docs for clarifications. To run the demo project: - create the database `./manage.py migrate` - run `./manage.py runserver 0.0.0.0:8000` - + or execute the run.sh script with these environment settings: ```` SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE=True SPID_TESTENV2_REMOTE_METADATA_ACTIVE=True bash run.sh ```` -If you choosed to use *spid-testenv2*, fefore starting it, you just have to save the +If you choosed to use *spid-testenv2*, fefore starting it, you just have to save the current demo metadata in *spid-testenv2* configuration, this way: ```` @@ -72,7 +71,7 @@ current demo metadata in *spid-testenv2* configuration, this way: wget http://localhost:8000/spid/metadata -O conf/sp_metadata.xml ```` -Finally, start spid-testenv2 and spid-saml-check (docker is suggested) and +Finally, start spid-testenv2 and spid-saml-check (docker is suggested) and then open 'http://localhost:8000' in your browser. @@ -90,14 +89,14 @@ then use `docker-compose --env-file docker-compose.env up` (the process takes so under `./example/configs/` to match the new configurations, changing only `./docker-compose.env` will not suffice. -Setup for an existing project +Setup for an existing project ----------------------------- djangosaml2_spid uses a pySAML2 fork. * `pip install git+https://github.com/peppelinux/pysaml2.git@pplnx-v6.5.1` * `pip install git+https://github.com/italia/spid-django` -* Copy the `example/spid_config/` to your project base dir +* Copy the `example/spid_config/` to your project base dir * Import SAML2 entity configuration in your project settings file: `from spid_config.spid_settings import *` * Add in `settings.INSTALLED_APPS` the following ``` @@ -135,15 +134,32 @@ SAML_ATTRIBUTE_MAPPING = { } ```` +Download identity providers metadata +----------------------------------- + +To update the list of entity providers use the custom django command `update_idps`. +In the example project you can do it as follows: + +```` +cd example/ +python ./manage.py update_idps +```` Running tests (only for developers) ----------------------------------- -Tests are integrated into the demo project and are intended for use +Tests are integrated into the demo project and are intended for use only by developers. +To test the application: +```` +pip install -r requirements-dev.txt +python runtests.py +```` + +For running tests using the settings of the Demo project: ```` -pip install requirements-dev.txt +pip install -r requirements-dev.txt cd example/ coverage erase coverage run ./manage.py test djangosaml2_spid.tests diff --git a/example/djangosaml2_spid b/example/djangosaml2_spid deleted file mode 120000 index d622b00..0000000 --- a/example/djangosaml2_spid +++ /dev/null @@ -1 +0,0 @@ -../src/djangosaml2_spid \ No newline at end of file diff --git a/example/example/settings.py b/example/example/settings.py index e7b8d5f..efb6e74 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -44,7 +44,7 @@ 'djangosaml2', 'djangosaml2_spid', - 'spid_config' + 'spid_config', ] MIDDLEWARE = [ diff --git a/example/example/urls.py b/example/example/urls.py index ad0266f..01c1af2 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,29 +1,11 @@ -"""example URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.1/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.contrib import admin from django.urls import path, include +from django.views.generic.base import RedirectView +import djangosaml2_spid.urls urlpatterns = [ path('admin/', admin.site.urls), + path('', include((djangosaml2_spid.urls, 'djangosaml2_spid',))), + path('', RedirectView.as_view(url=settings.SPID_BASE_URL), name='example-index') ] - -if 'djangosaml2_spid' in settings.INSTALLED_APPS: - import djangosaml2_spid.urls - - urlpatterns.extend([ - path('', include((djangosaml2_spid.urls, 'djangosaml2_spid',))) - ]) diff --git a/example/manage.py b/example/manage.py index 625a05a..a1ca4f5 100755 --- a/example/manage.py +++ b/example/manage.py @@ -19,4 +19,5 @@ def main(): if __name__ == '__main__': + __spec__ = None main() diff --git a/example/spid_config/spid_settings.py b/example/spid_config/spid_settings.py index f13992c..27390ba 100644 --- a/example/spid_config/spid_settings.py +++ b/example/spid_config/spid_settings.py @@ -1,22 +1,18 @@ +from saml2.saml import NAMEID_FORMAT_TRANSIENT +from saml2.sigver import get_xmlsec_binary import os import saml2 -from saml2.md import SamlBase -from saml2.saml import (NAMEID_FORMAT_PERSISTENT, - NAMEID_FORMAT_TRANSIENT, - NAMEID_FORMAT_UNSPECIFIED) -from saml2.sigver import get_xmlsec_binary # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -BASE_DIR_CERTS = os.environ.get('PWD') - -BASE = os.environ.get('SPID_BASE_SCHEMA_HOST_PORT', 'http://localhost:8000') -BASE_URL = '{}/spid'.format(BASE) +SPID_BASE_SCHEMA_HOST_PORT = os.environ.get('SPID_BASE_SCHEMA_HOST_PORT', 'http://localhost:8000') +SPID_URLS_PREFIX = 'spid' +SPID_BASE_URL = f'{SPID_BASE_SCHEMA_HOST_PORT}/{SPID_URLS_PREFIX}' -LOGIN_URL = '/spid/login/' -LOGOUT_URL = '/spid/logout/' -LOGIN_REDIRECT_URL = '/spid/echo_attributes' +LOGIN_URL = f'/{SPID_URLS_PREFIX}/login' +LOGOUT_URL = f'/{SPID_URLS_PREFIX}/logout' +LOGIN_REDIRECT_URL = f'/{SPID_URLS_PREFIX}/echo_attributes' LOGOUT_REDIRECT_URL = '/' SPID_DEFAULT_BINDING = saml2.BINDING_HTTP_POST @@ -25,114 +21,119 @@ SPID_NAMEID_FORMAT = NAMEID_FORMAT_TRANSIENT SPID_AUTH_CONTEXT = 'https://www.spid.gov.it/SpidL1' -SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE = os.environ.get('SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE', False) +SPID_CERTS_DIR = os.path.join(os.environ.get('PWD'), 'certificates/') +SPID_PUBLIC_CERT = os.path.join(SPID_CERTS_DIR, 'public.cert') +SPID_PRIVATE_KEY = os.path.join(SPID_CERTS_DIR, 'private.key') + +# source: https://registry.spid.gov.it/identity-providers +SPID_IDENTITY_PROVIDERS_URL = 'https://registry.spid.gov.it/assets/data/idp.json' +SPID_IDENTITY_PROVIDERS_METADATA_DIR = os.path.join(BASE_DIR, 'spid_config/metadata/') + +SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE = os.environ.get('SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE', 'False') == 'True' SPID_SAML_CHECK_METADATA_URL = os.environ.get('SPID_SAML_CHECK_METADATA_URL', 'http://localhost:8080/metadata.xml') -SPID_TESTENV2_REMOTE_METADATA_ACTIVE = os.environ.get('SPID_TESTENV2_REMOTE_METADATA_ACTIVE', False) +SPID_TESTENV2_REMOTE_METADATA_ACTIVE = os.environ.get('SPID_TESTENV2_REMOTE_METADATA_ACTIVE', 'False') == 'True' SPID_TESTENV2_METADATA_URL = os.environ.get('SPID_TESTENV2_METADATA_URL', 'http://localhost:8088/metadata') # Avviso 29v3 SPID_PREFIXES = dict( - spid = "https://spid.gov.it/saml-extensions", - fpa = "https://spid.gov.it/invoicing-extensions" + spid='https://spid.gov.it/saml-extensions', + fpa='https://spid.gov.it/invoicing-extensions' ) -# other or billing, not together at the same time! +APPEND_SLASH = True + SPID_CONTACTS = [ - # { - # 'contact_type': 'other', - # 'telephone_number': '+39 8475634785', - # 'email_address': 'tech-info@example.org', - # 'VATNumber': 'IT12345678901', - # 'FiscalCode': 'XYZABCAAMGGJ000W', - # 'Public': '' - # }, { - 'contact_type': 'other', - 'telephone_number': '+39 84756344785', - 'email_address': 'info@example.org', - 'VATNumber': 'IT12345678901', - 'FiscalCode': 'XYasdasdadasdGGJ000W', - 'Public': '' + 'contact_type': 'other', + 'telephone_number': '+39 8475634785', + 'email_address': 'tech-info@example.org', + 'VATNumber': 'IT12345678901', + 'FiscalCode': 'XYZABCAAMGGJ000W', + 'Private': '' }, # { - # 'contact_type': 'billing', - # 'telephone_number': '+39 84756344785', - # 'email_address': 'info@example.org', - # 'company': 'example s.p.a.', - # 'CodiceFiscale': 'NGLMRA80A01D086T', - # 'IdCodice': '983745349857', - # 'IdPaese': 'IT', - # 'Denominazione': 'Destinatario Fatturazione', - # 'Indirizzo': 'via tante cose', - # 'NumeroCivico': '12', - # 'CAP': '87100', - # 'Comune': 'Cosenza', - # 'Provincia': 'CS', - # 'Nazione': 'IT', + # 'contact_type': 'billing', + # 'telephone_number': '+39 84756344785', + # 'email_address': 'info@example.org', + # 'company': 'example s.p.a.', + ## 'CodiceFiscale': 'NGLMRA80A01D086T', + # 'IdCodice': '983745349857', + # 'IdPaese': 'IT', + # 'Denominazione': 'Destinatario Fatturazione', + # 'Indirizzo': 'via tante cose', + # 'NumeroCivico': '12', + # 'CAP': '87100', + # 'Comune': 'Cosenza', + # 'Provincia': 'CS', + # 'Nazione': 'IT', # }, ] SAML_CONFIG = { - 'debug' : True, - 'xmlsec_binary': get_xmlsec_binary(['/opt/local/bin', - '/usr/bin/xmlsec1']), - 'entityid': f'{BASE_URL}/metadata', - 'attribute_map_dir': f'{BASE_DIR}/spid_config/attribute-maps/', + 'debug': True, + 'xmlsec_binary': get_xmlsec_binary(['/opt/local/bin', '/usr/bin/xmlsec1']), + 'entityid': f'{SPID_BASE_URL}/metadata', + + # Attribute maps moved to src/djangosaml2_spid/attribute_maps/ + # 'attribute_map_dir': f'{BASE_DIR}/spid_config/attribute-maps/', 'service': { 'sp': { - 'name': f'{BASE_URL}/metadata', - 'name_qualifier': BASE, + 'name': f'{SPID_BASE_URL}/metadata/', + 'name_qualifier': SPID_BASE_SCHEMA_HOST_PORT, - # SPID needs NAMEID_FORMAT_TRANSIENT 'name_id_format': [SPID_NAMEID_FORMAT], 'endpoints': { 'assertion_consumer_service': [ - ('%s/acs/' % BASE_URL, saml2.BINDING_HTTP_POST), - ], - "single_logout_service": [ - ("%s/ls/post/" % BASE_URL, saml2.BINDING_HTTP_POST), - # ("%s/ls/" % BASE_URL, saml2.BINDING_HTTP_REDIRECT), + (f'{SPID_BASE_URL}/acs/', saml2.BINDING_HTTP_POST), ], - }, # end endpoints + 'single_logout_service': [ + (f'{SPID_BASE_URL}/ls/post/', saml2.BINDING_HTTP_POST), + # (f'{SPID_BASE_URL}/ls/', saml2.BINDING_HTTP_REDIRECT), + ], + }, - # Mandates that the identity provider MUST authenticate the - # presenter directly rather than rely on a previous security context. - "force_authn": False, # SPID + # Mandates that the IdP MUST authenticate the presenter directly + # rather than rely on a previous security context. + 'force_authn': False, # SPID 'name_id_format_allow_create': False, # attributes that this project need to identify a user - 'required_attributes': ['spidCode', - 'name', - 'familyName', - 'fiscalNumber', - 'email'], + 'required_attributes': [ + 'spidCode', + 'name', + 'familyName', + 'fiscalNumber', + 'email' + ], 'requested_attribute_name_format': saml2.saml.NAME_FORMAT_BASIC, 'name_format': saml2.saml.NAME_FORMAT_BASIC, - # # attributes that may be useful to have but not required - 'optional_attributes': ['gender', - 'companyName', - 'registeredOffice', - 'ivaCode', - 'idCard', - 'digitalAddress', - 'placeOfBirth', - 'countyOfBirth', - 'dateOfBirth', - 'address', - 'mobilePhone', - 'expirationDate'], - - 'signing_algorithm': saml2.xmldsig.SIG_RSA_SHA256, - 'digest_algorithm': saml2.xmldsig.DIGEST_SHA256, + 'optional_attributes': [ + 'gender', + 'companyName', + 'registeredOffice', + 'ivaCode', + 'idCard', + 'digitalAddress', + 'placeOfBirth', + 'countyOfBirth', + 'dateOfBirth', + 'address', + 'mobilePhone', + 'expirationDate' + ], + + 'signing_algorithm': SPID_SIG_ALG, + 'digest_algorithm': SPID_DIG_ALG, 'authn_requests_signed': True, 'logout_requests_signed': True, + # Indicates that Authentication Responses to this SP must # be signed. If set to True, the SP will not consume # any SAML Responses that are not signed. @@ -146,33 +147,33 @@ # Permits to have attributes not configured in attribute-mappings # otherwise...without OID will be rejected 'allow_unknown_attributes': True, - }, # end sp - + }, }, # many metadata, many idp... 'metadata': { - 'local': [f'{BASE_DIR}/spid_config/metadata'], - "remote": [] + 'local': [ + SPID_IDENTITY_PROVIDERS_METADATA_DIR + ], + 'remote': [] }, # Signing - 'key_file': f'{BASE_DIR_CERTS}/certificates/private.key', - 'cert_file': f'{BASE_DIR_CERTS}/certificates/public.cert', + 'key_file': SPID_PRIVATE_KEY, + 'cert_file': SPID_PUBLIC_CERT, # Encryption 'encryption_keypairs': [{ - 'key_file': f'{BASE_DIR_CERTS}/certificates/private.key', - 'cert_file': f'{BASE_DIR_CERTS}/certificates/public.cert', + 'key_file': SPID_PRIVATE_KEY, + 'cert_file': SPID_PUBLIC_CERT, }], # you can set multilanguage information here 'organization': { - 'name': [('Example', 'it'), ('Example', 'en')], - 'display_name': [('Example', 'it'), ('Example', 'en')], - 'url': [('http://www.example.it', 'it'), ('http://www.example.it', 'en')], - }, - + 'name': [('Example', 'it'), ('Example', 'en')], + 'display_name': [('Example', 'it'), ('Example', 'en')], + 'url': [('http://www.example.it', 'it'), ('http://www.example.it', 'en')], + }, } if SPID_SAML_CHECK_REMOTE_METADATA_ACTIVE: @@ -187,7 +188,7 @@ # OR NAME_ID or MAIN_ATTRIBUTE (not together!) SAML_USE_NAME_ID_AS_USERNAME = False -# + SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'username' SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP = '__iexact' @@ -197,11 +198,8 @@ SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST SAML_ATTRIBUTE_MAPPING = { - # 'username': ('fiscalNumber',), - # 'email': ('email', ), - # 'first_name': ('name'), - - 'fiscalNumber': ('username', ), + 'spidCode': ('username', ), + 'fiscalNumber': ('tin', ), 'email': ('email', ), 'name': ('first_name', ), 'familyName': ('last_name', ), diff --git a/example/spid_config/templates/wayf.html b/example/spid_config/templates/wayf.html index 49e7363..68fdbab 100644 --- a/example/spid_config/templates/wayf.html +++ b/example/spid_config/templates/wayf.html @@ -1,4 +1,4 @@ -{% load spid %} +{% load spid static %} @@ -14,19 +14,19 @@ - + - + - + - + @@ -121,7 +121,7 @@
- + SPID Logo
@@ -261,7 +261,7 @@

Benvenuto in Nome Organizzazione Spid Discovery Service

@@ -275,24 +275,24 @@

Benvenuto in Nome Organizzazione Spid Discovery Service