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 0000000..394102f Binary files /dev/null and b/vendor/spid-cie-oidc-django/spid_cie_oidc/onboarding/static/images/SPID-e-CIE-696x383.jpg differ 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 0000000..a6bfd6a Binary files /dev/null and b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/images/logo-cie.png differ diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/cie-ico-circle-bb.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/cie-ico-circle-bb.svg new file mode 100644 index 0000000..e0648f0 --- /dev/null +++ b/vendor/spid-cie-oidc-django/spid_cie_oidc/provider/static/svg/cie-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/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 0000000..f63204c Binary files /dev/null and b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/favicon.ico differ diff --git a/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/static/svg/spid-ico-circle-bb.svg b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party/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/relying_party/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/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 0000000..3e898d9 Binary files /dev/null and b/vendor/spid-cie-oidc-django/spid_cie_oidc/relying_party_test/static/images/oops.webp differ 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 "" + )