Skip to content

Commit

Permalink
spid-django in pypi (#79)
Browse files Browse the repository at this point in the history
* chore: spid metadata as django class view

* chore: README setup additional informations and tips

* - fix: spid-django example project now works also with django 2.2
- chore: removed spid-testenv2 from README
- feat: added 'allow_unsolicited` as customizable parameter in SAML_CONF  SETTING

* feat: CIE metadata endpoint and attributes (#80)

* feat: CIE metadata endpoint and attributes

* fix: removed useless test regarding the metadata web path

* v1.1.0
Giuseppe De Marco authored Jul 19, 2021
1 parent d2fa512 commit 85593fe
Showing 9 changed files with 219 additions and 49 deletions.
71 changes: 54 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
SPID Django
SPID/CIE 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)
![License](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9-blue.svg)


A SPID Service Provider based on [pysaml2](https://github.com/identitypython/pysaml2).
A SPID/CIE Service Provider based on [pysaml2](https://github.com/identitypython/pysaml2).


Introduction
------------

This is a Django application that provides a SAML2 Service Provider
for a Single Sign On with SPID, the Italian Digital Identity System.
for a Single Sign On with SPID and CIE, the Italian Digital Identity System.

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.
This project comes with a demo on a Spid button template with *spid-saml-check* IDP preconfigured.
See running the Demo project paragaph for details.

Furthermore, this application integrates the checks of
[Spid QA](https://www.spid.gov.it/assets/download/SPID_QAD.pdf)
within its CI pipeline, through [spid-sp-test](https://github.com/peppelinux/spid-sp-test).
See github actions log for details.

The technical documentation on SPID and SAML is available at [Docs Italia](https://docs.italia.it/italia/spid/spid-regole-tecniche)
The technical documentation on CIE and SAML is available at [Docs Italia](https://docs.italia.it/italia/cie/cie-manuale-tecnico-docs)


![big picture](gallery/animated.gif)

@@ -45,15 +47,24 @@ Running the Demo project
------------------------

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.
This project uses **spid-saml-check** as demo IDP.

Prepare environment:
````
cd example/
virtualenv -ppython3 env
source env/bin/activate
pip install -r ../requirements-dev.txt
pip install --no-deps djangosaml2-spid
pip install djangosaml2-spid
````

⚠️ Why `pip install` have beed executed twice? spid-django needs a fork of PySAML2 that's not distribuited though pypi.
This way to install it prevents the following error:

````
ERROR: Packages installed from PyPI cannot depend on packages which are not also hosted on PyPI.
djangosaml2-spid depends on pysaml2@ git+https://github.com/peppelinux/[email protected]#pysaml2
````

Your example saml2 configuration is in `spid_config/spid_settings.py`.
@@ -66,20 +77,19 @@ To run the demo project:

or execute the run.sh script with these environment settings to enable tests IdPs:

````
SPID_SAML_CHECK_IDP_ACTIVE=True SPID_DEMO_IDP_ACTIVE=True bash run.sh
````
````
SPID_SAML_CHECK_IDP_ACTIVE=True SPID_DEMO_IDP_ACTIVE=True bash run.sh
````

If you chose to use *spid-testenv2*, before starting it, you just have to save the
current demo metadata in *spid-testenv2* configuration, this way:
If you chose to use your own demo IdP you just have to save the
current demo metadata in the demo IdP configuration, this way:

````
# cd into spid-testenv2/ base dir ...
# cd into demo IdP metadata folder ...
wget https://localhost:8000/spid/metadata -O conf/sp_metadata.xml
````

Finally, start spid-testenv2 and spid-saml-check (docker is suggested) and
then open 'https://localhost:8000' in your browser.
Finally, start pid-saml-check (docker is suggested) and open 'https://localhost:8000' in your browser.


Demo project with Docker
@@ -92,7 +102,7 @@ To use Docker compose environment, add to /etc/hosts this line:

then use `docker-compose --env-file docker-compose.env up` (the process takes some time) and when the services are up go to http://hostnet:8000/spid/login

**warning**: if you want to change ports of any of the docker-compose services (as, spid-testenv2, spid-saml-check) and/or the FQDN of the docker-compose default network gateway (defaults to `hostnet`) you need to change all the files
**warning**: if you want to change ports of any of the docker-compose services (as, spid-saml-check) and/or the FQDN of the docker-compose default network gateway (defaults to `hostnet`) you need to change all the files
under `./example/configs/` to match the new configurations, changing only `./docker-compose.env` will not suffice.


@@ -122,6 +132,29 @@ djangosaml2_spid uses a pySAML2 fork.
* Register the SP metadata to your test Spid IDPs
* Start the django server for tests `./manage.py runserver 0.0.0.0:8000`

SAML2 SPID compliant certificates
---------------------------------

Here an example about how to do that.

````
mkdir certificates && cd "$_"
spid-compliant-certificates generator \
--key-size 3072 \
--common-name "A.C.M.E" \
--days 365 \
--entity-id https://spid.acme.it \
--locality-name Roma \
--org-id "PA:IT-c_h501" \
--org-name "A Company Making Everything" \
--sector public \
--key-out private.key \
--crt-out public.cert
cd ../
````

Minimal SPID settings
---------------------

@@ -135,6 +168,7 @@ An example of a minimal configuration for SPID is the following:

```python
SAML_CONFIG = {
'entityid': 'https://your.spid.url/metadata',
'organization': {
'name': [('Example', 'it'), ('Example', 'en')],
'display_name': [('Example', 'it'), ('Example', 'en')],
@@ -165,6 +199,9 @@ SPID_CONTACTS = [
]
```

⚠️ In the example project, in `spid_settings.py` we found `disable_ssl_certificate_validation` set to True. This is only for test/development purpose and its usage means that the "remote metadata" won't validate the https certificates. That's something not intended for production environment, remote metadata must be avoided and the tls validation must be adopted.


Attribute Mapping
-----------------
Is necessary to maps SPID attributes to Django ones.
6 changes: 3 additions & 3 deletions example/example/settings.py
Original file line number Diff line number Diff line change
@@ -10,12 +10,12 @@
https://docs.djangoproject.com/en/3.1/ref/settings/
"""

from pathlib import Path
import os

from spid_config.spid_settings import *

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
@@ -103,7 +103,7 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'NAME': os.path.sep.join((BASE_DIR, 'db.sqlite3')),
}
}

33 changes: 33 additions & 0 deletions example/spid_config/spid_settings.py
Original file line number Diff line number Diff line change
@@ -82,6 +82,39 @@
]


CIE_CONTACTS = [
{
'contact_type': 'administrative',
'IPACode': 'that-IPA-code',
'VATNumber': 'IT12345678901',
'FiscalCode': 'XYZABCAAMGGJ000W',
'NACE2Code': '12.34.56',
'Municipality': 'H501',
'Province': 'CS',
'Country': 'IT',
'Company': 'same-to-OrganizationName-if-PA',
'telephone_number': '+398475634785',
'email_address': '[email protected]',
'Public': '',
},
{
'contact_type': 'technical',
'telephone_number': '+39 84756344785',
'email_address': '[email protected]',
'IPACode': 'that-IPA-code',
'VATNumber': 'IT12345678901',
'FiscalCode': 'XYZABCAAMGGJ000W',
'NACE2Code': '12.34.56',
'Municipality': 'H501',
'Province': 'CS',
'Country': 'IT',
'Company': 'same-to-OrganizationName-if-PA',
'telephone_number': '+398475634785',
'email_address': '[email protected]',
},
]


# Configuration for pysaml2 as managed by djangosaml2. For SPID SP service the most
# part is built dynamically from provided SPID_* settings and from SPID_* defaults.
SAML_CONFIG = {
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@

setup(
name="djangosaml2-spid",
version='1.0.1',
version='1.1.0',
description="Djangosaml2 SPID Service Provider",
long_description=README,
long_description_content_type='text/markdown',
49 changes: 41 additions & 8 deletions src/djangosaml2_spid/conf.py
Original file line number Diff line number Diff line change
@@ -36,14 +36,15 @@
raise ImproperlyConfigured("Formato improprio per la configurazione SAML2!")
elif "organization" not in settings.SAML_CONFIG:
raise ImproperlyConfigured(
"Mancano le informazioni sull'organizzazione " "nella configurazione SAML2!"
"Mancano le informazioni sull'organizzazione nella configurazione SAML2!"
)

#
# SPID settings with default values

settings.SPID_BASE_URL = getattr(settings, "SPID_BASE_URL", None)
settings.SPID_URLS_PREFIX = getattr(settings, "SPID_URLS_PREFIX", "spid")
settings.CIE_URLS_PREFIX = getattr(settings, "CIE_URLS_PREFIX", "cie")

settings.SPID_ACS_URL_PATH = getattr(
settings, "SPID_ACS_URL_PATH", f"{settings.SPID_URLS_PREFIX}/acs/"
@@ -57,6 +58,9 @@
settings.SPID_METADATA_URL_PATH = getattr(
settings, "SPID_METADATA_URL_PATH", f"{settings.SPID_URLS_PREFIX}/metadata/"
)
settings.CIE_METADATA_URL_PATH = getattr(
settings, "CIE_METADATA_URL_PATH", f"{settings.CIE_URLS_PREFIX}/metadata/"
)

settings.LOGIN_URL = getattr(settings, "LOGIN_URL", "/spid/login")
settings.LOGOUT_URL = getattr(settings, "LOGOUT_URL", "/spid/logout")
@@ -167,6 +171,14 @@
),
)

settings.CIE_PREFIXES = getattr(
settings,
"CIE_PREFIXES",
dict(
cie="https://www.cartaidentita.interno.gov.it/saml-extensions"
),
)

#
# Defaults for other SAML settings

@@ -219,6 +231,18 @@
],
)

settings.CIE_REQUIRED_ATTRIBUTES = getattr(
settings,
"CIE_REQUIRED_ATTRIBUTES",
[
"name",
"familyName",
"fiscalNumber",
"dateOfBirth",
],
)


# Attributes that may be useful to have but not required
settings.SPID_OPTIONAL_ATTRIBUTES = getattr(
settings,
@@ -242,16 +266,21 @@

def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig:
conf = SPConfig()
if request is None or not request.path.lstrip("/").startswith(
settings.SPID_URLS_PREFIX
):
if request is None:
# Not a SPID request: load SAML_CONFIG unchanged
conf.load(copy.deepcopy(settings.SAML_CONFIG))
return conf

# Build a SAML_CONFIG for SPID
base_url = settings.SPID_BASE_URL or request.build_absolute_uri("/")
metadata_url = urljoin(base_url, reverse("djangosaml2_spid:spid_metadata"))
metadata_url = urljoin(base_url, settings.SPID_METADATA_URL_PATH)

if settings.SPID_METADATA_URL_PATH in request.get_full_path():
_REQUIRED_ATTRIBUTES = settings.SPID_REQUIRED_ATTRIBUTES
_OPTIONAL_ATTRIBUTES = settings.SPID_OPTIONAL_ATTRIBUTES
else:
_REQUIRED_ATTRIBUTES = settings.CIE_REQUIRED_ATTRIBUTES
_OPTIONAL_ATTRIBUTES = []

saml_config = {
"entityid": metadata_url,
@@ -284,8 +313,10 @@ def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig:
"force_authn": False, # SPID
"name_id_format_allow_create": False,
# attributes that this project need to identify a user
"required_attributes": settings.SPID_REQUIRED_ATTRIBUTES,
"optional_attributes": settings.SPID_OPTIONAL_ATTRIBUTES,

"required_attributes": _REQUIRED_ATTRIBUTES,
"optional_attributes": _OPTIONAL_ATTRIBUTES,

"requested_attribute_name_format": saml2.saml.NAME_FORMAT_BASIC,
"name_format": saml2.saml.NAME_FORMAT_BASIC,
"signing_algorithm": settings.SPID_SIG_ALG,
@@ -300,7 +331,9 @@ def config_settings_loader(request: Optional[HttpRequest] = None) -> SPConfig:
# Responses, i.e. SAML Responses for which it has not sent
# a respective SAML Authentication Request. Set to True to
# let ACS endpoint work.
"allow_unsolicited": False,
"allow_unsolicited": settings.SAML_CONFIG.get(
"allow_unsolicited", False
),
# Permits to have attributes not configured in attribute-mappings
# otherwise...without OID will be rejected
"allow_unknown_attributes": True,
58 changes: 53 additions & 5 deletions src/djangosaml2_spid/spid_metadata.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
from saml2.sigver import security_context


def spid_sp_metadata(conf):
def italian_sp_metadata(conf, md_type:str="spid"):
metadata = entity_descriptor(conf)

# this will renumber acs starting from 0 and set index=0 as is_default
@@ -41,7 +41,10 @@ def spid_sp_metadata(conf):
service_name.lang = "it"
service_name.text = conf._sp_name

avviso_29_v3(metadata)
if md_type == 'spid':
spid_contacts_29_v3(metadata)
elif md_type == 'cie':
cie_contacts(metadata)

# metadata signature
secc = security_context(conf)
@@ -52,7 +55,7 @@ def spid_sp_metadata(conf):
return xmldoc


def avviso_29_v3(metadata):
def spid_contacts_29_v3(metadata):
"""
https://www.agid.gov.it/sites/default/files/repository_files/spid-avviso-n29v3-specifiche_sp_pubblici_e_privati_0.pdf
"""
@@ -89,8 +92,6 @@ def avviso_29_v3(metadata):

spid_extensions.children.append(ext)

spid_contact.extensions = spid_extensions

elif contact["contact_type"] == "billing":
contact_kwargs["company"] = contact["company"]
spid_contact.loadd(contact_kwargs)
@@ -148,3 +149,50 @@ def avviso_29_v3(metadata):

spid_contact.extensions = spid_extensions
metadata.contact_person.append(spid_contact)


def cie_contacts(metadata):
"""
"""

saml2.md.SamlBase.register_prefix(settings.CIE_PREFIXES)

contact_map = settings.CIE_CONTACTS
metadata.contact_person = []
for contact in contact_map:
cie_contact = saml2.md.ContactPerson()
cie_contact.contact_type = contact["contact_type"]
contact_kwargs = {
"email_address": [contact["email_address"]],
"telephone_number": [contact["telephone_number"]],
}
cie_extensions = saml2.ExtensionElement(
"Extensions", namespace="urn:oasis:names:tc:SAML:2.0:metadata"
)

if contact["contact_type"] == "administrative":
cie_contact.loadd(contact_kwargs)
contact_kwargs["contact_type"] = contact["contact_type"]
for k, v in contact.items():
if k in contact_kwargs:
continue
ext = saml2.ExtensionElement(
k, namespace=settings.CIE_PREFIXES["cie"], text=v
)
cie_extensions.children.append(ext)

elif contact["contact_type"] == "technical":
cie_contact.loadd(contact_kwargs)
contact_kwargs["contact_type"] = contact["contact_type"]
elements = {}
for k, v in contact.items():
if k in contact_kwargs:
continue
ext = saml2.ExtensionElement(
k, namespace=settings.CIE_PREFIXES["cie"], text=v
)
elements[k] = ext


cie_contact.extensions = cie_extensions
metadata.contact_person.append(cie_contact)
4 changes: 0 additions & 4 deletions src/djangosaml2_spid/tests.py
Original file line number Diff line number Diff line change
@@ -129,10 +129,6 @@ def test_get_config(self):
saml_config = get_config()
self.assertIsNone(saml_config.entityid)

request = self.factory.get("")
saml_config = get_config(request=request)
self.assertIsNone(saml_config.entityid)

# SPConfig for a SPID request
request = self.factory.get("/spid/metadata")
saml_config = get_config(request=request)
7 changes: 6 additions & 1 deletion src/djangosaml2_spid/urls.py
Original file line number Diff line number Diff line change
@@ -15,7 +15,12 @@
),
path(f"{SPID_URLS_PREFIX}/login/", views.spid_login, name="spid_login"),
path(f"{SPID_URLS_PREFIX}/logout/", views.spid_logout, name="spid_logout"),
path(settings.SPID_METADATA_URL_PATH, views.metadata_spid, name="spid_metadata"),
path(
settings.SPID_METADATA_URL_PATH,
views.MetadataSpidView.as_view(), name="spid_metadata"),
path(
settings.CIE_METADATA_URL_PATH,
views.MetadataCieView.as_view(), name="cie_metadata"),
path(
settings.SPID_ACS_URL_PATH,
views.AssertionConsumerServiceView.as_view(),
38 changes: 28 additions & 10 deletions src/djangosaml2_spid/views.py
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@

from .conf import settings
from .spid_errors import SpidError
from .spid_metadata import spid_sp_metadata
from .spid_metadata import italian_sp_metadata
from .spid_request import spid_sp_authn_request, SAML2_DEFAULT_BINDING
from .spid_validator import Saml2ResponseValidator
from .utils import repr_saml_request
@@ -263,15 +263,33 @@ def spid_logout(request, config_loader_path=None, **kwargs):
return HttpResponse(http_info["data"])


def metadata_spid(request, config_loader_path=None, valid_for=None):
"""Returns an XML with the SAML 2.0 metadata for this
SP as configured in the settings.py file.
"""
conf = get_config(config_loader_path, request)
xmldoc = spid_sp_metadata(conf)
return HttpResponse(
content=str(xmldoc).encode("utf-8"), content_type="text/xml; charset=utf8"
)
class MetadataSpidView(djangosaml2_views.View):
"""SPID dynamic Metadata endpoint"""

def dispatch(self, request, *args, **kwargs):
self.conf = get_config(getattr(settings, 'SAML_CONFIG_LOADER'), request)
return super().dispatch(request, *args, **kwargs)

def build_metadata(self):
metadata = italian_sp_metadata(self.conf, md_type='spid')
return metadata

def get(self, request, *args, **kwargs):
"""Returns an XML with the SAML 2.0 metadata for this
SP as configured in the settings.py file.
"""
xmldoc = self.build_metadata()
return HttpResponse(
content=str(xmldoc).encode("utf-8"),
content_type="text/xml; charset=utf8"
)


class MetadataCieView(MetadataSpidView):

def build_metadata(self):
metadata = italian_sp_metadata(self.conf, md_type='cie')
return metadata


class EchoAttributesView(

0 comments on commit 85593fe

Please sign in to comment.