From 7db6d1f4e3f73241a0fd008f1352bf2af3b0b06f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 16:36:05 +0100 Subject: [PATCH 01/63] add oidc4vp module --- idhub/management/commands/initial_datas.py | 2 +- idhub/models.py | 16 ----- idhub/user/forms.py | 5 +- oidc4vp/__init__.py | 0 oidc4vp/admin.py | 3 + oidc4vp/apps.py | 6 ++ oidc4vp/forms.py | 41 ++++++++++++ oidc4vp/migrations/__init__.py | 0 oidc4vp/models.py | 77 ++++++++++++++++++++++ oidc4vp/tests.py | 3 + oidc4vp/views.py | 17 +++++ trustchain_idhub/settings.py | 2 + 12 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 oidc4vp/__init__.py create mode 100644 oidc4vp/admin.py create mode 100644 oidc4vp/apps.py create mode 100644 oidc4vp/forms.py create mode 100644 oidc4vp/migrations/__init__.py create mode 100644 oidc4vp/models.py create mode 100644 oidc4vp/tests.py create mode 100644 oidc4vp/views.py diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index acdf6c7..5de2603 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -5,7 +5,7 @@ from pathlib import Path from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from decouple import config -from idhub.models import Organization +from oidc4vp.models import Organization User = get_user_model() diff --git a/idhub/models.py b/idhub/models.py index 53f8186..09ba43b 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,6 +1,5 @@ import json import pytz -import requests import datetime from django.db import models from django.conf import settings @@ -639,18 +638,3 @@ class UserRol(models.Model): class Meta: unique_together = ('user', 'service',) - - -class Organization(models.Model): - name = models.CharField(max_length=250) - url = models.CharField( - help_text=_("Url where to send the presentation"), - max_length=250 - ) - - def __str__(self): - return self.name - - def send(self, cred): - return - requests.post(self.url, data=cred.data) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 53a1149..57e3d36 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,7 +1,7 @@ from django import forms from idhub_auth.models import User -from idhub.models import DID, VerificableCredential, Organization - +from idhub.models import DID, VerificableCredential +from oidc4vp.models import Organization class ProfileForm(forms.ModelForm): @@ -56,7 +56,6 @@ class RequestCredentialForm(forms.Form): return - class CredentialPresentationForm(forms.Form): organization = forms.ChoiceField(choices=[]) credential = forms.ChoiceField(choices=[]) diff --git a/oidc4vp/__init__.py b/oidc4vp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc4vp/admin.py b/oidc4vp/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/oidc4vp/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/oidc4vp/apps.py b/oidc4vp/apps.py new file mode 100644 index 0000000..1e00f2b --- /dev/null +++ b/oidc4vp/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class Oidc4VpConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'oidc4vp' diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py new file mode 100644 index 0000000..f4f56d5 --- /dev/null +++ b/oidc4vp/forms.py @@ -0,0 +1,41 @@ +from django import forms + + +class Organization(forms.Form): + wallet = forms.ChoiceField( + "Wallet", + choices=[(x.id, x.name) for x in Organization.objects.all()] + ) + + def clean_wallet(self): + data = self.cleaned_data["wallet"] + organization = Organization.objects.filter( + id=data + ) + + if not organization.exists(): + raise ValidationError("organization is not valid!") + + self.organization = organization.first() + + return data + + def authorize(self): + data = { + "response_type": "vp_token", + "response_mode": "direct_post", + "client_id": self.organization.client_id, + "response_uri": settings.RESPONSE_URI, + "presentation_definition": self.pv_definition(), + "nonce": "" + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + url = '{response_uri}/authorize?{params}'.format( + response_uri=self.organization.response_uri, + params=query_dict.urlencode() + ) + + def pv_definition(self): + return "" diff --git a/oidc4vp/migrations/__init__.py b/oidc4vp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc4vp/models.py b/oidc4vp/models.py new file mode 100644 index 0000000..ac6da6d --- /dev/null +++ b/oidc4vp/models.py @@ -0,0 +1,77 @@ +import requests + +from django.db import models +from django.http import QueryDict +from django.utils.translation import gettext_lazy as _ +from idhub_auth.models import User + + +class Organization(models.Model): + name = models.CharField(max_length=250) + client_id = models.CharField() + client_secret = models.CharField() + response_uri = models.URLField( + help_text=_("Url where to send the presentation"), + max_length=250 + ) + + def __str__(self): + return self.name + + def send(self, vcred): + return requests.post(self.url, data=vcred) + + +class Authorization(models.Model): + created = models.DateTimeField(auto_now=True) + presentation_definition = models.CharField() + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + ) + + def authorize(self): + response_uri = self.__class__.objects.filter( + response_uri=settings.RESPONSE_URI + ) + data = { + "response_type": "vp_token", + "response_mode": "direct_post", + "client_id": "...", + "response_uri": response_uri, + "presentation_definition": "...", + "nonce": "" + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + url = '{response_uri}/authorize?{params}'.format( + response_uri=self.organization.response_uri, + params=query_dict.urlencode() + ) + +class OAuth2VPToken(models.Model): + created = models.DateTimeField(auto_now=True) + response_code = models.CharField() + result_verify = models.BooleanField() + presentation_definition = models.CharField() + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + diff --git a/oidc4vp/tests.py b/oidc4vp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/oidc4vp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/oidc4vp/views.py b/oidc4vp/views.py new file mode 100644 index 0000000..42e1a58 --- /dev/null +++ b/oidc4vp/views.py @@ -0,0 +1,17 @@ +from django.shortcuts import render + +class PeopleEditView(People, FormView): + template_name = "idhub/admin/user_edit.html" + form_class = ProfileForm + success_url = reverse_lazy('idhub:admin_people_list') + + + def form_valid(self, form): + user = form.save() + messages.success(self.request, _('The credential was sended successfully')) + # Event.set_EV_USR_UPDATED_BY_ADMIN(user) + # Event.set_EV_USR_UPDATED(user) + + return super().form_valid(form) + + diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 30bbc0a..df36198 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -71,6 +71,7 @@ INSTALLED_APPS = [ 'django_extensions', 'django_bootstrap5', 'idhub_auth', + 'oidc4vp', 'idhub' ] @@ -183,3 +184,4 @@ USE_I18N = True USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' +RESPONSE_URI = config('RESPONSE_URI', default="") From 9e8596f39d07d1b0567647c52c0720ade273cc2c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 17:53:43 +0100 Subject: [PATCH 02/63] add more details to flow --- oidc4vp/migrations/0001_initial.py | 134 +++++++++++++++++++++++++++++ oidc4vp/models.py | 86 +++++++++++++++--- 2 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 oidc4vp/migrations/0001_initial.py diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py new file mode 100644 index 0000000..cc0d8fd --- /dev/null +++ b/oidc4vp/migrations/0001_initial.py @@ -0,0 +1,134 @@ +# Generated by Django 4.2.5 on 2023-11-24 16:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import oidc4vp.models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Authorization', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ( + 'code', + models.CharField(default=oidc4vp.models.set_code, max_length=24), + ), + ('created', models.DateTimeField(auto_now=True)), + ('presentation_definition', models.CharField(max_length=250)), + ], + ), + migrations.CreateModel( + name='Organization', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('name', models.CharField(max_length=250)), + ( + 'client_id', + models.CharField( + default=oidc4vp.models.set_client_id, max_length=24 + ), + ), + ( + 'client_secret', + models.CharField( + default=oidc4vp.models.set_client_secret, max_length=48 + ), + ), + ( + 'response_uri', + models.URLField( + help_text='Url where to send the verificable presentation', + max_length=250, + ), + ), + ], + ), + migrations.CreateModel( + name='OAuth2VPToken', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('created', models.DateTimeField(auto_now=True)), + ('code', models.CharField(max_length=250)), + ('result_verify', models.BooleanField(max_length=250)), + ('presentation_definition', models.CharField(max_length=250)), + ( + 'authorization', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='oidc4vp.authorization', + ), + ), + ( + 'organization', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='vp_tokens', + to='oidc4vp.organization', + ), + ), + ( + 'user', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='vp_tokens', + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name='authorization', + name='organization', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='authorizations', + to='oidc4vp.organization', + ), + ), + migrations.AddField( + model_name='authorization', + name='user', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ac6da6d..fa273b7 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -1,34 +1,80 @@ import requests +import secrets -from django.db import models +from django.conf import settings from django.http import QueryDict from django.utils.translation import gettext_lazy as _ from idhub_auth.models import User +from django.db import models + + +SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + +def gen_salt(length: int) -> str: + """Generate a random string of SALT_CHARS with specified ``length``.""" + if length <= 0: + raise ValueError("Salt length must be positive") + + return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) + + +def set_client_id(): + return gen_salt(24) + + +def set_client_secret(): + return gen_salt(48) + + +def set_code(): + return gen_salt(24) class Organization(models.Model): + """ + This class represent a member of one net trust or federated host + """ name = models.CharField(max_length=250) - client_id = models.CharField() - client_secret = models.CharField() + client_id = models.CharField(max_length=24, default=set_client_id) + client_secret = models.CharField(max_length=48, default=set_client_secret) response_uri = models.URLField( - help_text=_("Url where to send the presentation"), + help_text=_("Url where to send the verificable presentation"), max_length=250 ) + def send(self, vp): + """ + Send the verificable presentation to Verifier + """ + org = Organization.objects.get( + response_uri=settings.RESPONSE_URI + ) + auth = (org.client_id, org.client_secret) + return requests.post(self.url, data=vp, auth=auth) + def __str__(self): return self.name - def send(self, vcred): - return requests.post(self.url, data=vcred) + +################### +# Verifier clases # +################### class Authorization(models.Model): + """ + This class represent a query through browser the client to the wallet. + The Verifier need to do a redirection to the user to Wallet. + The code we use as a soft foreing key between Authorization and OAuth2VPToken. + """ + code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) - presentation_definition = models.CharField() + presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( Organization, on_delete=models.CASCADE, - related_name='vp_tokens', + related_name='authorizations', null=True, ) user = models.ForeignKey( @@ -44,10 +90,10 @@ class Authorization(models.Model): data = { "response_type": "vp_token", "response_mode": "direct_post", - "client_id": "...", + "client_id": self.organization.client_id, "response_uri": response_uri, "presentation_definition": "...", - "nonce": "" + "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) query_dict.update(data) @@ -56,12 +102,18 @@ class Authorization(models.Model): response_uri=self.organization.response_uri, params=query_dict.urlencode() ) + return url + class OAuth2VPToken(models.Model): + """ + This class represent the response of Wallet to Verifier + and the result of verify. + """ created = models.DateTimeField(auto_now=True) - response_code = models.CharField() - result_verify = models.BooleanField() - presentation_definition = models.CharField() + code = models.CharField(max_length=250) + result_verify = models.BooleanField(max_length=250) + presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -74,4 +126,12 @@ class OAuth2VPToken(models.Model): related_name='vp_tokens', null=True, ) + authorization = models.ForeignKey( + Authorization, + on_delete=models.SET_NULL, + null=True, + ) + + def verifing(self): + pass From b182c86a622d10853931277b7ec5a8b5e8ed8f79 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 18:10:43 +0100 Subject: [PATCH 03/63] fix nullable field --- oidc4vp/migrations/0001_initial.py | 4 ++-- oidc4vp/models.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index cc0d8fd..c88d0f8 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-24 16:53 +# Generated by Django 4.2.5 on 2023-11-24 17:10 from django.conf import settings from django.db import migrations, models @@ -50,7 +50,7 @@ class Migration(migrations.Migration): ( 'client_id', models.CharField( - default=oidc4vp.models.set_client_id, max_length=24 + default=oidc4vp.models.set_client_id, max_length=24, unique=True ), ), ( diff --git a/oidc4vp/models.py b/oidc4vp/models.py index fa273b7..e26464a 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -36,8 +36,15 @@ class Organization(models.Model): This class represent a member of one net trust or federated host """ name = models.CharField(max_length=250) - client_id = models.CharField(max_length=24, default=set_client_id) - client_secret = models.CharField(max_length=48, default=set_client_secret) + client_id = models.CharField( + max_length=24, + default=set_client_id, + unique=True + ) + client_secret = models.CharField( + max_length=48, + default=set_client_secret + ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), max_length=250 From 7e99c0ca9f0ce3cf288993c8ee15793280aa72a7 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:34:09 +0100 Subject: [PATCH 04/63] Testing branches --- utils/idhub_ssikit/README.md | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/utils/idhub_ssikit/README.md b/utils/idhub_ssikit/README.md index e69de29..8443717 100644 --- a/utils/idhub_ssikit/README.md +++ b/utils/idhub_ssikit/README.md @@ -0,0 +1,73 @@ +# Helper routines to manage DIDs/VC/VPs + +This module is a wrapper around the functions exported by SpruceID's `DIDKit` framework. + +## DID generation and storage + +For now DIDs are of the kind `did:key`, with planned support for `did:web` in the near future. + +Creation of a DID involves two steps: +* Generate a unique DID controller key +* Derive a `did:key` type from the key + +Both must be stored in the IdHub database and linked to a `User` for later retrieval. + +```python +# Use case: generate and link a new DID for an existing user +user = request.user # ... + +controller_key = idhub_ssikit.generate_did_controller_key() +did_string = idhub_ssikit.keydid_from_controller_key(controller_key) + + +did = idhub.models.DID( + did = did_string, + user = user +) +did_controller_key = idhub.models.DIDControllerKey( + key_material = controller_key, + owner_did = did +) + +did.save() +did_controller_key.save() +``` + +## Verifiable Credential issuance + +Verifiable Credential templates are stored as Jinja2 (TBD) templates in `/schemas` folder. Please examine each template to see what data must be passed to it in order to render. + +The data passed to the template must at a minimum include: +* issuer_did +* subject_did +* vc_id + +For example, in order to render `/schemas/member-credential.json`: + +```python +from jinja2 import Environment, FileSystemLoader, select_autoescape +import idhub_ssikit + +env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() +) +unsigned_vc_template = env.get_template("member-credential.json") + +issuer_user = request.user +issuer_did = user.dids[0] # TODO: Django ORM pseudocode +issuer_did_controller_key = did.keys[0] # TODO: Django ORM pseudocode + +data = { + "vc_id": "http://pangea.org/credentials/3731", + "issuer_did": issuer_did, + "subject_did": "did:web:[...]", + "issuance_date": "2020-08-19T21:41:50Z", + "subject_is_member_of": "Pangea" +} +signed_credential = idhub_ssikit.render_and_sign_credential( + unsigned_vc_template, + issuer_did_controller_key, + data +) +``` \ No newline at end of file From fa5a2d172eef55c65af54cb8dc7763c3c13af44a Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:36:58 +0100 Subject: [PATCH 05/63] Added some VC templates --- idhub/templates/credentials/exo.json | 30 +++++++++++++++++ idhub/templates/credentials/openarms.json | 33 +++++++++++++++++++ idhub/templates/credentials/paremanel.json | 30 +++++++++++++++++ .../credentials/verifiable_presentation.json | 11 +++++++ 4 files changed, 104 insertions(+) create mode 100644 idhub/templates/credentials/exo.json create mode 100644 idhub/templates/credentials/openarms.json create mode 100644 idhub/templates/credentials/paremanel.json create mode 100644 idhub/templates/credentials/verifiable_presentation.json diff --git a/idhub/templates/credentials/exo.json b/idhub/templates/credentials/exo.json new file mode 100644 index 0000000..1aee10b --- /dev/null +++ b/idhub/templates/credentials/exo.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "street_address": "https://schema.org/streetAddress", + "connectivity_option_list": "https://schema.org/connectivityOptionList", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "HomeConnectivitySurveyCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "street_address": "{{ street_address }}", + "connectivity_option_list": "{{ connectivity_option_list }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/openarms.json b/idhub/templates/credentials/openarms.json new file mode 100644 index 0000000..e2ecbc6 --- /dev/null +++ b/idhub/templates/credentials/openarms.json @@ -0,0 +1,33 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "destination_country": "https://schema.org/destinationCountry", + "offboarding_date": "https://schema.org/offboardingDate", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "MigrantRescueCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "name": "{{ name }}", + "country_of_origin": "{{ country_of_origin }}", + "rescue_date": "{{ rescue_date }}", + "destination_country": "{{ destination_country }}", + "offboarding_date": "{{ offboarding_date }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/paremanel.json b/idhub/templates/credentials/paremanel.json new file mode 100644 index 0000000..a40396b --- /dev/null +++ b/idhub/templates/credentials/paremanel.json @@ -0,0 +1,30 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + { + "name": "https://schema.org/name", + "email": "https://schema.org/email", + "membershipType": "https://schema.org/memberOf", + "individual": "https://schema.org/Person", + "organization": "https://schema.org/Organization", + "Member": "https://schema.org/Member", + "startDate": "https://schema.org/startDate", + "jsonSchema": "https://schema.org/jsonSchema", + "street_address": "https://schema.org/streetAddress", + "financial_vulnerability_score": "https://schema.org/financialVulnerabilityScore", + "$ref": "https://schema.org/jsonSchemaRef" + } + ], + "id": "{{ vc_id }}", + "type": ["VerifiableCredential", "FinancialSituationCredential"], + "issuer": "{{ issuer_did }}", + "issuanceDate": "{{ issuance_date }}", + "credentialSubject": { + "id": "{{ subject_did }}", + "street_address": "{{ street_address }}", + "financial_vulnerability_score": "{{ financial_vulnerability_score }}", + "jsonSchema": { + "$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json" + } + } +} diff --git a/idhub/templates/credentials/verifiable_presentation.json b/idhub/templates/credentials/verifiable_presentation.json new file mode 100644 index 0000000..752affb --- /dev/null +++ b/idhub/templates/credentials/verifiable_presentation.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "http://example.org/presentations/3731", + "type": [ + "VerifiablePresentation" + ], + "holder": "{{ holder_did }}", + "verifiableCredential": {{ verifiable_credential_list }} +} From 737d2a7dceb93e885775db96c2bfd318b8019ba8 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:04:30 +0100 Subject: [PATCH 06/63] Verifier portal backchannel endpoint --- idhub/urls.py | 4 +++ idhub/verification_portal/__init__.py | 0 idhub/verification_portal/models.py | 21 +++++++++++++ idhub/verification_portal/views.py | 43 +++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 69 insertions(+) create mode 100644 idhub/verification_portal/__init__.py create mode 100644 idhub/verification_portal/models.py create mode 100644 idhub/verification_portal/views.py diff --git a/idhub/urls.py b/idhub/urls.py index 785f4d1..80dd103 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy from .views import LoginView from .admin import views as views_admin from .user import views as views_user +from .verification_portal import views as views_verification_portal app_name = 'idhub' @@ -171,4 +172,7 @@ urlpatterns = [ name='admin_import'), path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), + + path('verification_portal/verify/', views_verification_portal.verify, + name="verification_portal_verify") ] diff --git a/idhub/verification_portal/__init__.py b/idhub/verification_portal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py new file mode 100644 index 0000000..c8e1615 --- /dev/null +++ b/idhub/verification_portal/models.py @@ -0,0 +1,21 @@ +from django.db import models + + +class VPVerifyRequest(models.Model): + """ + `nonce` is an opaque random string used to lookup verification requests + `expected_credentials` is a JSON list of credential types that must be present in this VP. + Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] + `expected_contents` is a JSON object that places optional constraints on the contents of the + returned VP. + Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] + `action` is (for now) a JSON object describing the next steps to take if this verification + is successful. For example "send mail to with and " + Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification + """ + nonce = models.CharField(max_length=50) + expected_credentials = models.CharField(max_length=255) + expected_contents = models.TextField() + action = models.TextField() + submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py new file mode 100644 index 0000000..afc922b --- /dev/null +++ b/idhub/verification_portal/views.py @@ -0,0 +1,43 @@ +import json + +from django.core.mail import send_mail +from django.http import HttpResponse +from .models import VPVerifyRequest +from django.shortcuts import get_object_or_404 +from more_itertools import flatten, unique_everseen + + +def verify(request): + assert request.method == "POST" + # TODO: use request.POST["presentation_submission"] + vp = json.loads(request.POST["vp_token"]) + nonce = vp["nonce"] + # "vr" = verification_request + vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 + # Get a list of all included verifiable credential types + included_credential_types = unique_everseen(flatten([ + vc["type"] for vc in vp["verifiableCredential"] + ])) + # Check that it matches what we requested + for requested_vc_type in json.loads(vr.expected_credentials): + if requested_vc_type not in included_credential_types: + raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error + # Perform whatever action we have to do + action = json.loads(vr.action) + if action["action"] == "send_mail": + subject = action["params"]["subject"] + to_email = action["params"]["to"] + from_email = "noreply@verifier-portal" + body = request.POST["vp-token"] + send_mail( + subject, + body, + from_email, + [to_email] + ) + elif action["action"] == "something-else": + pass + else: + raise Exception("Unknown action!") + return HttpResponse("OK! Your verifiable presentation was successfully presented.") + diff --git a/requirements.txt b/requirements.txt index 745c483..83aea96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +more-itertools==10.1.0 From 2c4dca40b7639665159a0e3bac0cb6d554be0a38 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:11:39 +0100 Subject: [PATCH 07/63] minor changes to verifier --- idhub/verification_portal/models.py | 5 ++++- idhub/verification_portal/views.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py index c8e1615..0bd203a 100644 --- a/idhub/verification_portal/models.py +++ b/idhub/verification_portal/models.py @@ -3,7 +3,8 @@ from django.db import models class VPVerifyRequest(models.Model): """ - `nonce` is an opaque random string used to lookup verification requests + `nonce` is an opaque random string used to lookup verification requests. URL-safe. + Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" `expected_credentials` is a JSON list of credential types that must be present in this VP. Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] `expected_contents` is a JSON object that places optional constraints on the contents of the @@ -12,10 +13,12 @@ class VPVerifyRequest(models.Model): `action` is (for now) a JSON object describing the next steps to take if this verification is successful. For example "send mail to with and " Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `response` is a URL that the user's wallet will redirect the user to. `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification """ nonce = models.CharField(max_length=50) expected_credentials = models.CharField(max_length=255) expected_contents = models.TextField() action = models.TextField() + response_or_redirect = models.CharField(max_length=255) submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index afc922b..df7cab2 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -1,7 +1,7 @@ import json from django.core.mail import send_mail -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 from more_itertools import flatten, unique_everseen @@ -39,5 +39,6 @@ def verify(request): pass else: raise Exception("Unknown action!") - return HttpResponse("OK! Your verifiable presentation was successfully presented.") + # OK! Your verifiable presentation was successfully presented. + return HttpResponseRedirect(vr.response_or_redirect) From 8566098f2c36a919d32b7e430f4bc59d0533cd34 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:42:12 +0100 Subject: [PATCH 08/63] Added verify_presentation bindings and use them in verification_portal backend --- idhub/verification_portal/views.py | 7 +++++- utils/idhub_ssikit/__init__.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index df7cab2..486f4f7 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -2,6 +2,8 @@ import json from django.core.mail import send_mail from django.http import HttpResponse, HttpResponseRedirect + +from utils.idhub_ssikit import verify_presentation from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 from more_itertools import flatten, unique_everseen @@ -9,7 +11,10 @@ from more_itertools import flatten, unique_everseen def verify(request): assert request.method == "POST" - # TODO: use request.POST["presentation_submission"] + # TODO: incorporate request.POST["presentation_submission"] as schema definition + (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) + if not presentation_valid: + raise Exception("Failed to verify signature on the given Verifiable Presentation.") vp = json.loads(request.POST["vp_token"]) nonce = vp["nonce"] # "vr" = verification_request diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 18a5ff2..c4ac0e3 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -72,3 +72,40 @@ def verify_credential(vc, proof_options): return didkit.verify_credential(vc, proof_options) return asyncio.run(inner()) + + +def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: + async def inner(): + unsigned_vp = unsigned_vp_template.render(data) + signed_vp = await didkit.issue_presentation( + unsigned_vp, + '{"proofFormat": "ldp"}', + jwk_holder + ) + return signed_vp + + # TODO: convert from Jinja2 -> django-templates + env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() + ) + unsigned_vp_template = env.get_template("verifiable_presentation.json") + data = { + "holder_did": holder_did, + "verifiable_credential_list": "[" + ",".join(vc_list) + "]" + } + + return asyncio.run(inner()) + + +def verify_presentation(vp): + """ + Returns a (bool, str) tuple indicating whether the credential is valid. + If the boolean is true, the credential is valid and the second argument can be ignored. + If it is false, the VC is invalid and the second argument contains a JSON object with further information. + """ + async def inner(): + proof_options = '{"proofFormat": "ldp"}' + return didkit.verify_presentation(vp, proof_options) + + return asyncio.run(inner()) From 545bf76f46051271da487241f6ad97ed22050052 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Nov 2023 10:59:30 +0100 Subject: [PATCH 09/63] merge ssi --- oidc4vp/models.py | 28 +++++++++++++++++++++++ oidc4vp/views.py | 58 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index e26464a..5432aa3 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -75,6 +75,12 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ + nonce = models.CharField(max_length=50) + expected_credentials = models.CharField(max_length=255) + expected_contents = models.TextField() + action = models.TextField() + response_or_redirect = models.CharField(max_length=255) + code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) @@ -142,3 +148,25 @@ class OAuth2VPToken(models.Model): def verifing(self): pass + +class VPVerifyRequest(models.Model): + """ + `nonce` is an opaque random string used to lookup verification requests. URL-safe. + Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" + `expected_credentials` is a JSON list of credential types that must be present in this VP. + Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] + `expected_contents` is a JSON object that places optional constraints on the contents of the + returned VP. + Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] + `action` is (for now) a JSON object describing the next steps to take if this verification + is successful. For example "send mail to with and " + Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `response` is a URL that the user's wallet will redirect the user to. + `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification + """ + nonce = models.CharField(max_length=50) + expected_credentials = models.CharField(max_length=255) + expected_contents = models.TextField() + action = models.TextField() + response_or_redirect = models.CharField(max_length=255) + submitted_on = models.DateTimeField(auto_now=True) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 42e1a58..486f4f7 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,17 +1,49 @@ -from django.shortcuts import render +import json -class PeopleEditView(People, FormView): - template_name = "idhub/admin/user_edit.html" - form_class = ProfileForm - success_url = reverse_lazy('idhub:admin_people_list') +from django.core.mail import send_mail +from django.http import HttpResponse, HttpResponseRedirect + +from utils.idhub_ssikit import verify_presentation +from .models import VPVerifyRequest +from django.shortcuts import get_object_or_404 +from more_itertools import flatten, unique_everseen - def form_valid(self, form): - user = form.save() - messages.success(self.request, _('The credential was sended successfully')) - # Event.set_EV_USR_UPDATED_BY_ADMIN(user) - # Event.set_EV_USR_UPDATED(user) - - return super().form_valid(form) - +def verify(request): + assert request.method == "POST" + # TODO: incorporate request.POST["presentation_submission"] as schema definition + (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) + if not presentation_valid: + raise Exception("Failed to verify signature on the given Verifiable Presentation.") + vp = json.loads(request.POST["vp_token"]) + nonce = vp["nonce"] + # "vr" = verification_request + vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 + # Get a list of all included verifiable credential types + included_credential_types = unique_everseen(flatten([ + vc["type"] for vc in vp["verifiableCredential"] + ])) + # Check that it matches what we requested + for requested_vc_type in json.loads(vr.expected_credentials): + if requested_vc_type not in included_credential_types: + raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error + # Perform whatever action we have to do + action = json.loads(vr.action) + if action["action"] == "send_mail": + subject = action["params"]["subject"] + to_email = action["params"]["to"] + from_email = "noreply@verifier-portal" + body = request.POST["vp-token"] + send_mail( + subject, + body, + from_email, + [to_email] + ) + elif action["action"] == "something-else": + pass + else: + raise Exception("Unknown action!") + # OK! Your verifiable presentation was successfully presented. + return HttpResponseRedirect(vr.response_or_redirect) From 19e44cf52d6de6052c39f1bf389db3499a78778f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 09:39:02 +0100 Subject: [PATCH 10/63] demand authorization --- examples/organizations.csv | 2 + idhub/management/commands/initial_datas.py | 2 +- idhub/templates/idhub/base.html | 2 +- idhub/urls.py | 9 +- idhub/user/forms.py | 47 ++++++-- idhub/user/views.py | 30 ++++- oidc4vp/models.py | 29 +++-- .../credentials/verifiable_presentation.json | 0 oidc4vp/views.py | 105 ++++++++++++------ trustchain_idhub/settings.py | 1 + trustchain_idhub/urls.py | 1 + 11 files changed, 171 insertions(+), 57 deletions(-) rename {idhub => oidc4vp}/templates/credentials/verifiable_presentation.json (100%) diff --git a/examples/organizations.csv b/examples/organizations.csv index 61f0f82..e79edda 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,2 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" +"test2";"http://localhost:9000/verify" +"test1";"http://localhost:8000/verify" diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 5de2603..2ce6640 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -41,4 +41,4 @@ class Command(BaseCommand): def create_organizations(self, name, url): - Organization.objects.create(name=name, url=url) + Organization.objects.create(name=name, response_uri=url) diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index c8d985b..3514c33 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -115,7 +115,7 @@ diff --git a/idhub/urls.py b/idhub/urls.py index 80dd103..48b9214 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -20,7 +20,7 @@ from django.urls import path, reverse_lazy from .views import LoginView from .admin import views as views_admin from .user import views as views_user -from .verification_portal import views as views_verification_portal +# from .verification_portal import views as views_verification_portal app_name = 'idhub' @@ -85,6 +85,9 @@ urlpatterns = [ path('user/credentials/request/', views_user.CredentialsRequestView.as_view(), name='user_credentials_request'), + path('user/credentials_presentation/demand', + views_user.DemandAuthorizationView.as_view(), + name='user_demand_authorization'), path('user/credentials_presentation/', views_user.CredentialsPresentationView.as_view(), name='user_credentials_presentation'), @@ -173,6 +176,6 @@ urlpatterns = [ path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), - path('verification_portal/verify/', views_verification_portal.verify, - name="verification_portal_verify") + # path('verification_portal/verify/', views_verification_portal.verify, + # name="verification_portal_verify") ] diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 57e3d36..8bacf89 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,4 +1,6 @@ +import requests from django import forms +from django.conf import settings from idhub_auth.models import User from idhub.models import DID, VerificableCredential from oidc4vp.models import Organization @@ -56,9 +58,40 @@ class RequestCredentialForm(forms.Form): return +class DemandAuthorizationForm(forms.Form): + organization = forms.ChoiceField(choices=[]) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] + + def save(self, commit=True): + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): + return + + self.org = self.org[0] + + if commit: + url = self.org.demand_authorization() + auth = (self.org.client_id, self.org.client_secret) + # res = requests.get(url, auth=auth) + # import pdb; pdb.set_trace() + # if res.status == 200: + # return res.body + + return + + class CredentialPresentationForm(forms.Form): organization = forms.ChoiceField(choices=[]) - credential = forms.ChoiceField(choices=[]) + # credential = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) @@ -66,12 +99,12 @@ class CredentialPresentationForm(forms.Form): self.fields['organization'].choices = [ (x.id, x.name) for x in Organization.objects.filter() ] - self.fields['credential'].choices = [ - (x.id, x.type()) for x in VerificableCredential.objects.filter( - user=self.user, - status=VerificableCredential.Status.ISSUED - ) - ] + # self.fields['credential'].choices = [ + # (x.id, x.type()) for x in VerificableCredential.objects.filter( + # user=self.user, + # status=VerificableCredential.Status.ISSUED + # ) + # ] def save(self, commit=True): self.org = Organization.objects.filter( diff --git a/idhub/user/views.py b/idhub/user/views.py index 482b40e..f509016 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -12,7 +12,12 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages -from idhub.user.forms import ProfileForm, RequestCredentialForm, CredentialPresentationForm +from idhub.user.forms import ( + ProfileForm, + RequestCredentialForm, + CredentialPresentationForm, + DemandAuthorizationForm +) from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event @@ -141,6 +146,28 @@ class CredentialsRequestView(MyWallet, FormView): return super().form_valid(form) +class DemandAuthorizationView(MyWallet, FormView): + template_name = "idhub/user/credentials_presentation.html" + subtitle = _('Credential presentation') + icon = 'bi bi-patch-check-fill' + form_class = DemandAuthorizationForm + success_url = reverse_lazy('idhub:user_demand_authorization') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + authorization = form.save() + if authorization: + if authorization.get('redirect_uri'): + redirect(authorization.get('redirect_uri')) + else: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + + class CredentialsPresentationView(MyWallet, FormView): template_name = "idhub/user/credentials_presentation.html" subtitle = _('Credential presentation') @@ -151,6 +178,7 @@ class CredentialsPresentationView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + kwargs['authorize'] = self.request.GET.params.get("uri") return kwargs def form_valid(self, form): diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 5432aa3..2d0d224 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -54,12 +54,27 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ - org = Organization.objects.get( + org = self.__class__.objects.get( response_uri=settings.RESPONSE_URI ) auth = (org.client_id, org.client_secret) return requests.post(self.url, data=vp, auth=auth) + def demand_authorization(self): + """ + Send the a request for start a process of Verifier + """ + org = self.__class__.objects.get( + response_uri=settings.RESPONSE_URI + ) + # import pdb; pdb.set_trace() + url = "{url}/?demand_uri={redirect_uri}".format( + url=self.response_uri.strip("/"), + redirect_uri=settings.RESPONSE_URI + ) + auth = (org.client_id, org.client_secret) + return requests.get(url, auth=auth) + def __str__(self): return self.name @@ -75,11 +90,11 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ - nonce = models.CharField(max_length=50) - expected_credentials = models.CharField(max_length=255) - expected_contents = models.TextField() - action = models.TextField() - response_or_redirect = models.CharField(max_length=255) + # nonce = models.CharField(max_length=50) + # expected_credentials = models.CharField(max_length=255) + # expected_contents = models.TextField() + # action = models.TextField() + # response_or_redirect = models.CharField(max_length=255) code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) @@ -98,7 +113,7 @@ class Authorization(models.Model): def authorize(self): response_uri = self.__class__.objects.filter( - response_uri=settings.RESPONSE_URI + response_uri=settings.ALLOW_CODE_URI ) data = { "response_type": "vp_token", diff --git a/idhub/templates/credentials/verifiable_presentation.json b/oidc4vp/templates/credentials/verifiable_presentation.json similarity index 100% rename from idhub/templates/credentials/verifiable_presentation.json rename to oidc4vp/templates/credentials/verifiable_presentation.json diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 486f4f7..c217114 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -6,44 +6,75 @@ from django.http import HttpResponse, HttpResponseRedirect from utils.idhub_ssikit import verify_presentation from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 -from more_itertools import flatten, unique_everseen +# from more_itertools import flatten, unique_everseen +from oidc4vp.models import Authorization + + +# class PeopleListView(People, TemplateView): +# template_name = "idhub/admin/people.html" +# subtitle = _('View users') +# icon = 'bi bi-person' + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# context.update({ +# 'users': User.objects.filter(), +# }) +# return context + + +def DemandAuthorizationView(request): + assert request.method == "GET" + import pdb; pdb.set_trace() + params = request.GET.params + org = Organization.objects.filter( + url=params.get('redirect_uri') + ) + authorization = Authorization( + organization=org, + presentation_definition="MemberCredential" + ) + # authorization.save() + res = json.dumps({"uri": authorization.authorize()}) + return HttpResponse(res) def verify(request): - assert request.method == "POST" - # TODO: incorporate request.POST["presentation_submission"] as schema definition - (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) - if not presentation_valid: - raise Exception("Failed to verify signature on the given Verifiable Presentation.") - vp = json.loads(request.POST["vp_token"]) - nonce = vp["nonce"] - # "vr" = verification_request - vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 - # Get a list of all included verifiable credential types - included_credential_types = unique_everseen(flatten([ - vc["type"] for vc in vp["verifiableCredential"] - ])) - # Check that it matches what we requested - for requested_vc_type in json.loads(vr.expected_credentials): - if requested_vc_type not in included_credential_types: - raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error - # Perform whatever action we have to do - action = json.loads(vr.action) - if action["action"] == "send_mail": - subject = action["params"]["subject"] - to_email = action["params"]["to"] - from_email = "noreply@verifier-portal" - body = request.POST["vp-token"] - send_mail( - subject, - body, - from_email, - [to_email] - ) - elif action["action"] == "something-else": - pass - else: - raise Exception("Unknown action!") - # OK! Your verifiable presentation was successfully presented. - return HttpResponseRedirect(vr.response_or_redirect) + import pdb; pdb.set_trace() +# assert request.method == "POST" +# # TODO: incorporate request.POST["presentation_submission"] as schema definition +# (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) +# if not presentation_valid: +# raise Exception("Failed to verify signature on the given Verifiable Presentation.") +# vp = json.loads(request.POST["vp_token"]) +# nonce = vp["nonce"] +# # "vr" = verification_request +# vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 +# # Get a list of all included verifiable credential types +# included_credential_types = unique_everseen(flatten([ +# vc["type"] for vc in vp["verifiableCredential"] +# ])) +# # Check that it matches what we requested +# for requested_vc_type in json.loads(vr.expected_credentials): +# if requested_vc_type not in included_credential_types: +# raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error +# # Perform whatever action we have to do +# action = json.loads(vr.action) +# if action["action"] == "send_mail": +# subject = action["params"]["subject"] +# to_email = action["params"]["to"] +# from_email = "noreply@verifier-portal" +# body = request.POST["vp-token"] +# send_mail( +# subject, +# body, +# from_email, +# [to_email] +# ) +# elif action["action"] == "something-else": +# pass +# else: +# raise Exception("Unknown action!") +# # OK! Your verifiable presentation was successfully presented. +# return HttpResponseRedirect(vr.response_or_redirect) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 4d90de7..ca45d06 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -186,3 +186,4 @@ USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' RESPONSE_URI = config('RESPONSE_URI', default="") +ALLOW_CODE_URI= config('ALLOW_CODE_URI', default="") diff --git a/trustchain_idhub/urls.py b/trustchain_idhub/urls.py index f2fdc05..1668872 100644 --- a/trustchain_idhub/urls.py +++ b/trustchain_idhub/urls.py @@ -24,4 +24,5 @@ from django.contrib.auth import views as auth_views urlpatterns = [ # path('django-admin/', admin.site.urls), path('', include('idhub.urls')), + path('oidc4vp/', include('oidc4vp.urls')), ] From a976b5bd16669ef72ad6891fab9322e7c7465fc6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 10:48:57 +0100 Subject: [PATCH 11/63] view demand --- oidc4vp/urls.py | 12 ++++++++++++ oidc4vp/views.py | 31 +++++++++++++++---------------- 2 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 oidc4vp/urls.py diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py new file mode 100644 index 0000000..d06949c --- /dev/null +++ b/oidc4vp/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, reverse_lazy + +from oidc4vp import views + + +app_name = 'oidc4vp' + + +urlpatterns = [ + path('verify/', views.VerifyView.as_view(), + name="verification_portal_verify") +] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index c217114..4a39fba 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,26 +1,25 @@ import json -from django.core.mail import send_mail -from django.http import HttpResponse, HttpResponseRedirect +from django.views.generic.edit import View -from utils.idhub_ssikit import verify_presentation -from .models import VPVerifyRequest -from django.shortcuts import get_object_or_404 -# from more_itertools import flatten, unique_everseen from oidc4vp.models import Authorization +from django.http import HttpResponse -# class PeopleListView(People, TemplateView): -# template_name = "idhub/admin/people.html" -# subtitle = _('View users') -# icon = 'bi bi-person' +# from django.core.mail import send_mail +# from django.http import HttpResponse, HttpResponseRedirect -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context.update({ -# 'users': User.objects.filter(), -# }) -# return context +# from utils.idhub_ssikit import verify_presentation +# from oidc4vp.models import VPVerifyRequest +# from django.shortcuts import get_object_or_404 +# from more_itertools import flatten, unique_everseen + + +class VerifyView(View): + def get(self, request, *args, **kwargs): + import pdb; pdb.set_trace() + res = json.dumps({"uri": "http://localhost:10000"}) + return HttpResponse(res) def DemandAuthorizationView(request): From cfbbaf491eb9909265533f661900b7db4647223d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 12:48:50 +0100 Subject: [PATCH 12/63] fix organization example --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index e79edda..70e1af0 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/verify" -"test1";"http://localhost:8000/verify" +"test2";"http://localhost:9000/oidc4vp/verify" +"test1";"http://localhost:8000/oidc4vp/verify" From 5e95d6b15c96cfb152eb4311e9fbff18dd4c8e96 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 12:49:28 +0100 Subject: [PATCH 13/63] demand authorization get to verifier --- oidc4vp/views.py | 105 +++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 59 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 4a39fba..6fe4623 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -2,7 +2,7 @@ import json from django.views.generic.edit import View -from oidc4vp.models import Authorization +from oidc4vp.models import Authorization, Organization from django.http import HttpResponse @@ -11,69 +11,56 @@ from django.http import HttpResponse # from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest -# from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen class VerifyView(View): def get(self, request, *args, **kwargs): + org_url = request.GET.get('demand_uri') + org = get_object_or_404(Organization, response_uri=org_url) + authorization = Authorization( + organization=org, + presentation_definition="MemberCredential" + ) import pdb; pdb.set_trace() - res = json.dumps({"uri": "http://localhost:10000"}) + res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) - -def DemandAuthorizationView(request): - assert request.method == "GET" - import pdb; pdb.set_trace() - params = request.GET.params - org = Organization.objects.filter( - url=params.get('redirect_uri') - ) - authorization = Authorization( - organization=org, - presentation_definition="MemberCredential" - ) - # authorization.save() - res = json.dumps({"uri": authorization.authorize()}) - return HttpResponse(res) - - -def verify(request): - import pdb; pdb.set_trace() -# assert request.method == "POST" -# # TODO: incorporate request.POST["presentation_submission"] as schema definition -# (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) -# if not presentation_valid: -# raise Exception("Failed to verify signature on the given Verifiable Presentation.") -# vp = json.loads(request.POST["vp_token"]) -# nonce = vp["nonce"] -# # "vr" = verification_request -# vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 -# # Get a list of all included verifiable credential types -# included_credential_types = unique_everseen(flatten([ -# vc["type"] for vc in vp["verifiableCredential"] -# ])) -# # Check that it matches what we requested -# for requested_vc_type in json.loads(vr.expected_credentials): -# if requested_vc_type not in included_credential_types: -# raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error -# # Perform whatever action we have to do -# action = json.loads(vr.action) -# if action["action"] == "send_mail": -# subject = action["params"]["subject"] -# to_email = action["params"]["to"] -# from_email = "noreply@verifier-portal" -# body = request.POST["vp-token"] -# send_mail( -# subject, -# body, -# from_email, -# [to_email] -# ) -# elif action["action"] == "something-else": -# pass -# else: -# raise Exception("Unknown action!") -# # OK! Your verifiable presentation was successfully presented. -# return HttpResponseRedirect(vr.response_or_redirect) - + def post(self, request, *args, **kwargs): + import pdb; pdb.set_trace() + # # TODO: incorporate request.POST["presentation_submission"] as schema definition + # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) + # if not presentation_valid: + # raise Exception("Failed to verify signature on the given Verifiable Presentation.") + # vp = json.loads(request.POST["vp_token"]) + # nonce = vp["nonce"] + # # "vr" = verification_request + # vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 + # # Get a list of all included verifiable credential types + # included_credential_types = unique_everseen(flatten([ + # vc["type"] for vc in vp["verifiableCredential"] + # ])) + # # Check that it matches what we requested + # for requested_vc_type in json.loads(vr.expected_credentials): + # if requested_vc_type not in included_credential_types: + # raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error + # # Perform whatever action we have to do + # action = json.loads(vr.action) + # if action["action"] == "send_mail": + # subject = action["params"]["subject"] + # to_email = action["params"]["to"] + # from_email = "noreply@verifier-portal" + # body = request.POST["vp-token"] + # send_mail( + # subject, + # body, + # from_email, + # [to_email] + # ) + # elif action["action"] == "something-else": + # pass + # else: + # raise Exception("Unknown action!") + # # OK! Your verifiable presentation was successfully presented. + # return HttpResponseRedirect(vr.response_or_redirect) From d84ad8f47000901bec3ef6bcf8011f9a47cd0aa1 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 17:33:24 +0100 Subject: [PATCH 14/63] first step of oidc --- idhub/user/forms.py | 6 ++---- idhub/user/views.py | 4 ++-- oidc4vp/models.py | 6 +----- oidc4vp/views.py | 29 +++++++++++++++++++++++------ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 8bacf89..cce197c 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -81,10 +81,8 @@ class DemandAuthorizationForm(forms.Form): if commit: url = self.org.demand_authorization() auth = (self.org.client_id, self.org.client_secret) - # res = requests.get(url, auth=auth) - # import pdb; pdb.set_trace() - # if res.status == 200: - # return res.body + if url.status_code == 200: + return url.json().get('redirect_uri') return diff --git a/idhub/user/views.py b/idhub/user/views.py index f509016..09eae35 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -160,9 +160,9 @@ class DemandAuthorizationView(MyWallet, FormView): def form_valid(self, form): authorization = form.save() + # import pdb; pdb.set_trace() if authorization: - if authorization.get('redirect_uri'): - redirect(authorization.get('redirect_uri')) + redirect(authorization) else: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 2d0d224..83cdef1 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -112,15 +112,11 @@ class Authorization(models.Model): ) def authorize(self): - response_uri = self.__class__.objects.filter( - response_uri=settings.ALLOW_CODE_URI - ) data = { "response_type": "vp_token", "response_mode": "direct_post", "client_id": self.organization.client_id, - "response_uri": response_uri, - "presentation_definition": "...", + "presentation_definition": self.presentation_definition, "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 6fe4623..77a97b6 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,9 +1,11 @@ import json +import base64 from django.views.generic.edit import View from oidc4vp.models import Authorization, Organization -from django.http import HttpResponse +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404 # from django.core.mail import send_mail @@ -11,22 +13,37 @@ from django.http import HttpResponse # from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest -from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen class VerifyView(View): def get(self, request, *args, **kwargs): - org_url = request.GET.get('demand_uri') - org = get_object_or_404(Organization, response_uri=org_url) + org = self.validate(request) + if not org: + raise Http404("Page not Found!") + authorization = Authorization( organization=org, presentation_definition="MemberCredential" ) - import pdb; pdb.set_trace() - res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) + def validate(self, request): + auth_header = request.headers.get('Authorization', b'') + auth_data = auth_header.split() + + if len(auth_data) == 2 and auth_data[0].lower() == b'basic': + decoded_auth = base64.b64decode(auth_data[1]).decode('utf-8') + client_id, client_secret = decoded_auth.split(':', 1) + org_url = request.GET.get('demand_uri') + org = get_object_or_404( + Organization, + response_uri=org_url, + client_id=client_id, + client_secret=client_secret + ) + return org + def post(self, request, *args, **kwargs): import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition From 4a3cf5c5df86be6b662d369bb83738392e20683d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 10:54:05 +0100 Subject: [PATCH 15/63] fix models --- oidc4vp/migrations/0001_initial.py | 34 +++++++++++++++++++++++++++++- oidc4vp/models.py | 27 ++++++++++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index c88d0f8..917553d 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-24 17:10 +# Generated by Django 4.2.5 on 2023-11-29 09:52 from django.conf import settings from django.db import migrations, models @@ -59,6 +59,18 @@ class Migration(migrations.Migration): default=oidc4vp.models.set_client_secret, max_length=48 ), ), + ( + 'my_client_id', + models.CharField( + default=oidc4vp.models.set_client_id, max_length=24, unique=True + ), + ), + ( + 'my_client_secret', + models.CharField( + default=oidc4vp.models.set_client_secret, max_length=48 + ), + ), ( 'response_uri', models.URLField( @@ -68,6 +80,26 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name='VPVerifyRequest', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('nonce', models.CharField(max_length=50)), + ('expected_credentials', models.CharField(max_length=255)), + ('expected_contents', models.TextField()), + ('action', models.TextField()), + ('response_or_redirect', models.CharField(max_length=255)), + ('submitted_on', models.DateTimeField(auto_now=True)), + ], + ), migrations.CreateModel( name='OAuth2VPToken', fields=[ diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 83cdef1..3a711bf 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -33,7 +33,14 @@ def set_code(): class Organization(models.Model): """ - This class represent a member of one net trust or federated host + This class represent a member of one net trust or federated host. + Client_id and client_secret are the credentials of this organization + get a connection to my. (receive a request) + My_client_id and my_client_secret are my credentials than to use if I + want to connect to this organization. (send a request) + For use the packages requests we need use my_client_id + For use in the get or post method of a View, then we need use client_id + and secret_id """ name = models.CharField(max_length=250) client_id = models.CharField( @@ -45,6 +52,15 @@ class Organization(models.Model): max_length=48, default=set_client_secret ) + my_client_id = models.CharField( + max_length=24, + default=set_client_id, + unique=True + ) + my_client_secret = models.CharField( + max_length=48, + default=set_client_secret + ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), max_length=250 @@ -54,11 +70,8 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ - org = self.__class__.objects.get( - response_uri=settings.RESPONSE_URI - ) - auth = (org.client_id, org.client_secret) - return requests.post(self.url, data=vp, auth=auth) + auth = (self.my_client_id, self.client_secret) + return requests.post(self.response_uri, data=vp, auth=auth) def demand_authorization(self): """ @@ -72,7 +85,7 @@ class Organization(models.Model): url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) - auth = (org.client_id, org.client_secret) + auth = (self.my_client_id, self.client_secret) return requests.get(url, auth=auth) def __str__(self): From 37d3430680d0cf540cf4018a24e6f6b2627d241c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:18:12 +0100 Subject: [PATCH 16/63] fix credentials --- oidc4vp/models.py | 5 +---- oidc4vp/views.py | 9 +++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 3a711bf..94c30c5 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -54,12 +54,9 @@ class Organization(models.Model): ) my_client_id = models.CharField( max_length=24, - default=set_client_id, - unique=True ) my_client_secret = models.CharField( max_length=48, - default=set_client_secret ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), @@ -128,7 +125,7 @@ class Authorization(models.Model): data = { "response_type": "vp_token", "response_mode": "direct_post", - "client_id": self.organization.client_id, + "client_id": self.organization.my_client_id, "presentation_definition": self.presentation_definition, "nonce": gen_salt(5), } diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 77a97b6..7e8ce0e 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -19,13 +19,13 @@ from django.shortcuts import get_object_or_404 class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - if not org: - raise Http404("Page not Found!") - + # TODO Not hardcode the list of types of presentation_definition + presentation_definition = json.dumps(['MemberCredential']) authorization = Authorization( organization=org, - presentation_definition="MemberCredential" + presentation_definition=presentation_definition ) + res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) def validate(self, request): @@ -45,6 +45,7 @@ class VerifyView(View): return org def post(self, request, *args, **kwargs): + org = self.validate(request) import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) From c4fa16107c6c56dcdffcf043a2e7b740f1939757 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:19:17 +0100 Subject: [PATCH 17/63] fix migrations --- oidc4vp/migrations/0001_initial.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 917553d..230817d 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-29 09:52 +# Generated by Django 4.2.5 on 2023-11-29 10:18 from django.conf import settings from django.db import migrations, models @@ -59,18 +59,8 @@ class Migration(migrations.Migration): default=oidc4vp.models.set_client_secret, max_length=48 ), ), - ( - 'my_client_id', - models.CharField( - default=oidc4vp.models.set_client_id, max_length=24, unique=True - ), - ), - ( - 'my_client_secret', - models.CharField( - default=oidc4vp.models.set_client_secret, max_length=48 - ), - ), + ('my_client_id', models.CharField(max_length=24)), + ('my_client_secret', models.CharField(max_length=48)), ( 'response_uri', models.URLField( From f0bb7182bb2b1c352b5743aeb7f165ab2c2dba8a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:42:20 +0100 Subject: [PATCH 18/63] fix validate credentials --- oidc4vp/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 7e8ce0e..667b4cb 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -32,7 +32,7 @@ class VerifyView(View): auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() - if len(auth_data) == 2 and auth_data[0].lower() == b'basic': + if len(auth_data) == 2 and auth_data[0].lower() == 'basic': decoded_auth = base64.b64decode(auth_data[1]).decode('utf-8') client_id, client_secret = decoded_auth.split(':', 1) org_url = request.GET.get('demand_uri') From c6d0cd2b5b2a0b94a12e29fb6804c360e5086ca0 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:50:24 +0100 Subject: [PATCH 19/63] fix demand authorization --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 94c30c5..37a18f9 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -82,7 +82,7 @@ class Organization(models.Model): url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) - auth = (self.my_client_id, self.client_secret) + auth = (self.my_client_id, self.my_client_secret) return requests.get(url, auth=auth) def __str__(self): From 7d477a079b2735a69357a6a1b7e0ac876c730132 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:53:30 +0100 Subject: [PATCH 20/63] fix view demand authorization --- idhub/user/forms.py | 1 - idhub/user/views.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index cce197c..16151bb 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -80,7 +80,6 @@ class DemandAuthorizationForm(forms.Form): if commit: url = self.org.demand_authorization() - auth = (self.org.client_id, self.org.client_secret) if url.status_code == 200: return url.json().get('redirect_uri') diff --git a/idhub/user/views.py b/idhub/user/views.py index 09eae35..0d0cb35 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -160,9 +160,8 @@ class DemandAuthorizationView(MyWallet, FormView): def form_valid(self, form): authorization = form.save() - # import pdb; pdb.set_trace() if authorization: - redirect(authorization) + return redirect(authorization) else: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) From 222509c72c7a0fb5f119e80738a1f465995df932 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:06:53 +0100 Subject: [PATCH 21/63] suport credentials defined in settings --- oidc4vp/views.py | 4 ++-- trustchain_idhub/settings.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 667b4cb..045731f 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,6 +1,7 @@ import json import base64 +from django.conf import settings from django.views.generic.edit import View from oidc4vp.models import Authorization, Organization @@ -19,8 +20,7 @@ from django.shortcuts import get_object_or_404 class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - # TODO Not hardcode the list of types of presentation_definition - presentation_definition = json.dumps(['MemberCredential']) + presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, presentation_definition=presentation_definition diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index ca45d06..e25b340 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -187,3 +187,8 @@ USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' RESPONSE_URI = config('RESPONSE_URI', default="") ALLOW_CODE_URI= config('ALLOW_CODE_URI', default="") +SUPPORTED_CREDENTIALS = config( + 'SUPPORTED_CREDENTIALS', + default='[]', + cast=literal_eval +) From 5e76b4631f693c2b36d3deb780c777821002e10b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:27:20 +0100 Subject: [PATCH 22/63] add authorization endpoint --- oidc4vp/models.py | 10 +++++----- oidc4vp/urls.py | 6 ++++-- oidc4vp/views.py | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 37a18f9..f004494 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -67,18 +67,18 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ + url = "{url}/verify".format( + url=self.response_uri.strip("/"), + ) auth = (self.my_client_id, self.client_secret) - return requests.post(self.response_uri, data=vp, auth=auth) + return requests.post(url, data=vp, auth=auth) def demand_authorization(self): """ Send the a request for start a process of Verifier """ - org = self.__class__.objects.get( - response_uri=settings.RESPONSE_URI - ) # import pdb; pdb.set_trace() - url = "{url}/?demand_uri={redirect_uri}".format( + url = "{url}/verify?demand_uri={redirect_uri}".format( url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index d06949c..03010e8 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -7,6 +7,8 @@ app_name = 'oidc4vp' urlpatterns = [ - path('verify/', views.VerifyView.as_view(), - name="verification_portal_verify") + path('verify', views.VerifyView.as_view(), + name="verify"), + path('authorization', views.AuthorizationView.as_view(), + name="authorization"), ] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 045731f..7b1666d 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -17,6 +17,10 @@ from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen +class AuthorizationView(View): + pass + + class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) From 3ba9a7ab88170f49f45f9a7670121209a26ec6e8 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:32:58 +0100 Subject: [PATCH 23/63] fix not validate --- oidc4vp/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 7b1666d..8ad53a4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -24,6 +24,8 @@ class AuthorizationView(View): class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) + if not org: + return Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, From adfc4df13d868f7906e6065d1b51e13b00f3dde7 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:33:55 +0100 Subject: [PATCH 24/63] fix not validate --- oidc4vp/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 8ad53a4..8abbf66 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -25,7 +25,7 @@ class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) if not org: - return Http404("Organization not found!") + raise Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, From a1700a9d6eda519f3f59806d1b6d589d6bdeeebc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:37:17 +0100 Subject: [PATCH 25/63] fix slash --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index f004494..ee4e44a 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -133,7 +133,7 @@ class Authorization(models.Model): query_dict.update(data) url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri, + response_uri=self.organization.response_uri.strip("/"), params=query_dict.urlencode() ) return url From 4f1a5afea72a8f3aa8eca593a37feca90d1d6385 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 13:21:49 +0100 Subject: [PATCH 26/63] first step authorize --- idhub/templates/idhub/base.html | 2 +- oidc4vp/urls.py | 4 ++-- oidc4vp/views.py | 39 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index 3514c33..834b7ef 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -115,7 +115,7 @@ diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index 03010e8..b151a85 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -9,6 +9,6 @@ app_name = 'oidc4vp' urlpatterns = [ path('verify', views.VerifyView.as_view(), name="verify"), - path('authorization', views.AuthorizationView.as_view(), - name="authorization"), + path('authorize', views.AuthorizeView.as_view(), + name="authorize"), ] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 8abbf66..699f926 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -2,12 +2,18 @@ import json import base64 from django.conf import settings -from django.views.generic.edit import View +from django.views.generic.edit import View, FormView +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse_lazy from oidc4vp.models import Authorization, Organization -from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404 +from idhub.mixins import UserView +from idhub.user.forms import ( + DemandAuthorizationForm +) # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect @@ -17,15 +23,32 @@ from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen -class AuthorizationView(View): - pass +class AuthorizeView(UserView, FormView): + title = _("My wallet") + section = "MyWallet" + template_name = "credentials_presentation.html" + subtitle = _('Credential presentation') + icon = 'bi bi-patch-check-fill' + form_class = DemandAuthorizationForm + success_url = reverse_lazy('idhub:user_demand_authorization') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + authorization = form.save() + if authorization: + return redirect(authorization) + else: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - if not org: - raise Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, @@ -50,6 +73,8 @@ class VerifyView(View): ) return org + raise Http404("Organization not found!") + def post(self, request, *args, **kwargs): org = self.validate(request) import pdb; pdb.set_trace() From 89f1668c5c84adcc58560b800482d0c09fd6c230 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 13:22:13 +0100 Subject: [PATCH 27/63] fix response with problems in connections --- idhub/user/views.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index 0d0cb35..0a3bfde 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -159,7 +159,15 @@ class DemandAuthorizationView(MyWallet, FormView): return kwargs def form_valid(self, form): - authorization = form.save() + try: + authorization = form.save() + except Exception: + txt = _("Problems connecting with {url}").format( + url=form.org.response_uri + ) + messages.error(self.request, txt) + return super().form_valid(form) + if authorization: return redirect(authorization) else: From 8da426ef345d51afc35823ee08d5e49bec94ee2f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 17:29:31 +0100 Subject: [PATCH 28/63] add type in schemas --- idhub/admin/views.py | 13 ++++- idhub/migrations/0001_initial.py | 87 ++++++++++++---------------- idhub/models.py | 8 +-- oidc4vp/forms.py | 97 ++++++++++++++++++++++---------- oidc4vp/views.py | 12 ++-- 5 files changed, 123 insertions(+), 94 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index f8fd6d0..08e0052 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -772,11 +772,14 @@ class SchemasNewView(SchemasMix): return try: data = f.read().decode('utf-8') - assert credtools.validate_schema(json.loads(data)) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema @@ -818,10 +821,14 @@ class SchemasImportAddView(SchemasMix): data = self.open_file(file_name) try: json.loads(data) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index b4d6ac7..b163ccc 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2023-11-29 16:14 from django.conf import settings from django.db import migrations, models @@ -57,27 +57,6 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now=True)), ], ), - migrations.CreateModel( - name='Organization', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('name', models.CharField(max_length=250)), - ( - 'url', - models.CharField( - help_text='Url where to send the presentation', max_length=250 - ), - ), - ], - ), migrations.CreateModel( name='Rol', fields=[ @@ -111,6 +90,7 @@ class Migration(migrations.Migration): verbose_name='ID', ), ), + ('type', models.CharField(max_length=250)), ('file_schema', models.CharField(max_length=250)), ('data', models.TextField()), ('created_at', models.DateTimeField(auto_now=True)), @@ -274,36 +254,39 @@ class Migration(migrations.Migration): 'type', models.PositiveSmallIntegerField( choices=[ - (1, 'EV_USR_REGISTERED'), - (2, 'EV_USR_WELCOME'), - (3, 'EV_DATA_UPDATE_REQUESTED_BY_USER'), - (4, 'EV_DATA_UPDATE_REQUESTED'), - (5, 'EV_USR_UPDATED_BY_ADMIN'), - (6, 'EV_USR_UPDATED'), - (7, 'EV_USR_DELETED_BY_ADMIN'), - (8, 'EV_DID_CREATED_BY_USER'), - (9, 'EV_DID_CREATED'), - (10, 'EV_DID_DELETED'), - (11, 'EV_CREDENTIAL_DELETED_BY_ADMIN'), - (12, 'EV_CREDENTIAL_DELETED'), - (13, 'EV_CREDENTIAL_ISSUED_FOR_USER'), - (14, 'EV_CREDENTIAL_ISSUED'), - (15, 'EV_CREDENTIAL_PRESENTED_BY_USER'), - (16, 'EV_CREDENTIAL_PRESENTED'), - (17, 'EV_CREDENTIAL_ENABLED'), - (18, 'EV_CREDENTIAL_CAN_BE_REQUESTED'), - (19, 'EV_CREDENTIAL_REVOKED_BY_ADMIN'), - (20, 'EV_CREDENTIAL_REVOKED'), - (21, 'EV_ROLE_CREATED_BY_ADMIN'), - (22, 'EV_ROLE_MODIFIED_BY_ADMIN'), - (23, 'EV_ROLE_DELETED_BY_ADMIN'), - (24, 'EV_SERVICE_CREATED_BY_ADMIN'), - (25, 'EV_SERVICE_MODIFIED_BY_ADMIN'), - (26, 'EV_SERVICE_DELETED_BY_ADMIN'), - (27, 'EV_ORG_DID_CREATED_BY_ADMIN'), - (28, 'EV_ORG_DID_DELETED_BY_ADMIN'), - (29, 'EV_USR_DEACTIVATED_BY_ADMIN'), - (30, 'EV_USR_ACTIVATED_BY_ADMIN'), + (1, 'User registered'), + (2, 'User welcomed'), + (3, 'Data update requested by user'), + ( + 4, + 'Data update requested. Pending approval by administrator', + ), + (5, "User's data updated by admin"), + (6, 'Your data updated by admin'), + (7, 'User deactivated by admin'), + (8, 'DID created by user'), + (9, 'DID created'), + (10, 'DID deleted'), + (11, 'Credential deleted by user'), + (12, 'Credential deleted'), + (13, 'Credential issued for user'), + (14, 'Credential issued'), + (15, 'Credential presented by user'), + (16, 'Credential presented'), + (17, 'Credential enabled'), + (18, 'Credential available'), + (19, 'Credential revoked by admin'), + (20, 'Credential revoked'), + (21, 'Role created by admin'), + (22, 'Role modified by admin'), + (23, 'Role deleted by admin'), + (24, 'Service created by admin'), + (25, 'Service modified by admin'), + (26, 'Service deleted by admin'), + (27, 'Organisational DID created by admin'), + (28, 'Organisational DID deleted by admin'), + (29, 'User deactivated'), + (30, 'User activated'), ] ), ), diff --git a/idhub/models.py b/idhub/models.py index 09ba43b..384d3ed 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -431,6 +431,7 @@ class DID(models.Model): class Schemas(models.Model): + type = models.CharField(max_length=250) file_schema = models.CharField(max_length=250) data = models.TextField() created_at = models.DateTimeField(auto_now=True) @@ -492,10 +493,9 @@ class VerificableCredential(models.Model): return json.loads(self.data) def type(self): - if self.data: - return self.get_schema.get('type')[-1] - - return self.schema.name() + # if self.data and: + # return self.get_schema.get('type')[-1] + return self.schema.type def description(self): if not self.data: diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index f4f56d5..cdef22e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,41 +1,78 @@ from django import forms +from django.conf import settings + +from oidc4vp.models import Organization -class Organization(forms.Form): - wallet = forms.ChoiceField( - "Wallet", - choices=[(x.id, x.name) for x in Organization.objects.all()] - ) +# class OrganizationForm(forms.Form): +# wallet = forms.ChoiceField( +# "Wallet", +# choices=[(x.id, x.name) for x in Organization.objects.all()] +# ) - def clean_wallet(self): - data = self.cleaned_data["wallet"] - organization = Organization.objects.filter( - id=data - ) +# def clean_wallet(self): +# data = self.cleaned_data["wallet"] +# organization = Organization.objects.filter( +# id=data +# ) - if not organization.exists(): - raise ValidationError("organization is not valid!") +# if not organization.exists(): +# raise ValidationError("organization is not valid!") - self.organization = organization.first() +# self.organization = organization.first() - return data +# return data - def authorize(self): - data = { - "response_type": "vp_token", - "response_mode": "direct_post", - "client_id": self.organization.client_id, - "response_uri": settings.RESPONSE_URI, - "presentation_definition": self.pv_definition(), - "nonce": "" - } - query_dict = QueryDict('', mutable=True) - query_dict.update(data) +# def authorize(self): +# data = { +# "response_type": "vp_token", +# "response_mode": "direct_post", +# "client_id": self.organization.client_id, +# "response_uri": settings.RESPONSE_URI, +# "presentation_definition": self.pv_definition(), +# "nonce": "" +# } +# query_dict = QueryDict('', mutable=True) +# query_dict.update(data) - url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri, - params=query_dict.urlencode() +# url = '{response_uri}/authorize?{params}'.format( +# response_uri=self.organization.response_uri, +# params=query_dict.urlencode() +# ) + +# def pv_definition(self): +# return "" + + +class AuthorizeForm(forms.Form): + organization = forms.ChoiceField(choices=[]) + + def __init__(self, *args, **kwargs): + # import pdb; pdb.set_trace() + self.user = kwargs.pop('user', None) + self.presentation_definition = kwargs.pop('presentation_definition', []) + self.credentials = self.user.vcredentials.filter( + schema__type__in=self.presentation_definition ) + super().__init__(*args, **kwargs) + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] + + def save(self, commit=True): + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): + return + + self.org = self.org[0] + + if commit: + url = self.org.demand_authorization() + if url.status_code == 200: + return url.json().get('redirect_uri') + + return - def pv_definition(self): - return "" diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 699f926..5a5427e 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -11,9 +11,8 @@ from django.urls import reverse_lazy from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView -from idhub.user.forms import ( - DemandAuthorizationForm -) +from oidc4vp.forms import AuthorizeForm + # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect @@ -29,12 +28,15 @@ class AuthorizeView(UserView, FormView): template_name = "credentials_presentation.html" subtitle = _('Credential presentation') icon = 'bi bi-patch-check-fill' - form_class = DemandAuthorizationForm + form_class = AuthorizeForm success_url = reverse_lazy('idhub:user_demand_authorization') def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + vps = self.request.GET.get('presentation_definition') + # import pdb; pdb.set_trace() + kwargs['presentation_definition'] = json.loads(vps) return kwargs def form_valid(self, form): @@ -77,7 +79,7 @@ class VerifyView(View): def post(self, request, *args, **kwargs): org = self.validate(request) - import pdb; pdb.set_trace() + # import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) # if not presentation_valid: From d8c9e0b8b3880c2934cff192e8bfe83fdb048db4 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 17:10:21 +0100 Subject: [PATCH 29/63] get the credentials from form --- oidc4vp/forms.py | 42 ++++++++++++++++++++++++++---------------- oidc4vp/views.py | 1 + 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index cdef22e..9a8633e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -45,34 +45,44 @@ from oidc4vp.models import Organization class AuthorizeForm(forms.Form): - organization = forms.ChoiceField(choices=[]) + # organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): # import pdb; pdb.set_trace() + self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.presentation_definition = kwargs.pop('presentation_definition', []) + + reg = r'({})'.format('|'.join(self.presentation_definition)) + self.credentials = self.user.vcredentials.filter( - schema__type__in=self.presentation_definition + schema__type__iregex=reg ) super().__init__(*args, **kwargs) - self.fields['organization'].choices = [ - (x.id, x.name) for x in Organization.objects.filter() - if x.response_uri != settings.RESPONSE_URI - ] + for vp in self.presentation_definition: + vp = vp.lower() + choices = [ + (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( + schema__type__iexact=vp) + ] + self.fields[vp.lower()] = forms.ChoiceField( + widget=forms.RadioSelect, + choices=choices + ) def save(self, commit=True): - self.org = Organization.objects.filter( - id=self.data['organization'] - ) - if not self.org.exists(): - return + # self.org = Organization.objects.filter( + # id=self.data['organization'] + # ) + # if not self.org.exists(): + # return - self.org = self.org[0] + # self.org = self.org[0] - if commit: - url = self.org.demand_authorization() - if url.status_code == 200: - return url.json().get('redirect_uri') + # if commit: + # url = self.org.demand_authorization() + # if url.status_code == 200: + # return url.json().get('redirect_uri') return diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 5a5427e..48ea8b7 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -7,6 +7,7 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy +from django.contrib import messages from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView From 2c97bf8d36aab6f63fd6c6720461dfca4bfd62fd Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 18:27:11 +0100 Subject: [PATCH 30/63] fix url organizations --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index 70e1af0..45029bc 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/oidc4vp/verify" -"test1";"http://localhost:8000/oidc4vp/verify" +"test2";"http://localhost:9000/oidc4vp/" +"test1";"http://localhost:8000/oidc4vp/" From 37908ba1e70d9761a6dbe454fe7c258d8a7c1eb3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 19:31:09 +0100 Subject: [PATCH 31/63] generate presentation, step1 --- idhub/migrations/0001_initial.py | 11 ++- idhub/models.py | 6 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/forms.py | 83 ++++++---------- .../templates/credentials_presentation.html | 96 +++++++++++++++++++ utils/idhub_ssikit/__init__.py | 33 +++---- 6 files changed, 155 insertions(+), 76 deletions(-) create mode 100644 oidc4vp/templates/credentials_presentation.html diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 4fbf53d..0a49195 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 17:19 +# Generated by Django 4.2.5 on 2023-12-01 18:29 from django.conf import settings from django.db import migrations, models @@ -148,7 +148,6 @@ class Migration(migrations.Migration): ('verified', models.BooleanField()), ('created_on', models.DateTimeField(auto_now=True)), ('issued_on', models.DateTimeField(null=True)), - ('subject_did', models.CharField(max_length=250)), ('data', models.TextField()), ('csv_data', models.TextField()), ( @@ -179,6 +178,14 @@ class Migration(migrations.Migration): to='idhub.schemas', ), ), + ( + 'subject_did', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='subject_credentials', + to='idhub.did', + ), + ), ( 'user', models.ForeignKey( diff --git a/idhub/models.py b/idhub/models.py index 262f186..915929c 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -463,7 +463,6 @@ class VerificableCredential(models.Model): verified = models.BooleanField() created_on = models.DateTimeField(auto_now=True) issued_on = models.DateTimeField(null=True) - subject_did = models.CharField(max_length=250) data = models.TextField() csv_data = models.TextField() status = models.PositiveSmallIntegerField( @@ -475,6 +474,11 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) + subject_did = models.ForeignKey( + DID, + on_delete=models.CASCADE, + related_name='subject_credentials', + ) issuer_did = models.ForeignKey( DID, on_delete=models.CASCADE, diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 9091472..741a8b9 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 17:19 +# Generated by Django 4.2.5 on 2023-12-01 18:29 from django.db import migrations, models diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 9a8633e..3d4b16f 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,54 +1,13 @@ from django import forms from django.conf import settings +from utils.idhub_ssikit import issue_verifiable_presentation from oidc4vp.models import Organization -# class OrganizationForm(forms.Form): -# wallet = forms.ChoiceField( -# "Wallet", -# choices=[(x.id, x.name) for x in Organization.objects.all()] -# ) - -# def clean_wallet(self): -# data = self.cleaned_data["wallet"] -# organization = Organization.objects.filter( -# id=data -# ) - -# if not organization.exists(): -# raise ValidationError("organization is not valid!") - -# self.organization = organization.first() - -# return data - -# def authorize(self): -# data = { -# "response_type": "vp_token", -# "response_mode": "direct_post", -# "client_id": self.organization.client_id, -# "response_uri": settings.RESPONSE_URI, -# "presentation_definition": self.pv_definition(), -# "nonce": "" -# } -# query_dict = QueryDict('', mutable=True) -# query_dict.update(data) - -# url = '{response_uri}/authorize?{params}'.format( -# response_uri=self.organization.response_uri, -# params=query_dict.urlencode() -# ) - -# def pv_definition(self): -# return "" - - class AuthorizeForm(forms.Form): - # organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): - # import pdb; pdb.set_trace() self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.presentation_definition = kwargs.pop('presentation_definition', []) @@ -69,20 +28,36 @@ class AuthorizeForm(forms.Form): widget=forms.RadioSelect, choices=choices ) + def clean(self): + data = super().clean() + import pdb; pdb.set_trace() + self.list_credentials = [] + for c in self.credentials: + if str(c.id) == data.get(c.schema.type.lower()): + self.list_credentials.append(c) + return data def save(self, commit=True): - # self.org = Organization.objects.filter( - # id=self.data['organization'] - # ) - # if not self.org.exists(): - # return + if not self.list_credentials: + return - # self.org = self.org[0] + did = self.list_credentials[0].subject_did - # if commit: - # url = self.org.demand_authorization() - # if url.status_code == 200: - # return url.json().get('redirect_uri') - - return + self.vp = issue_verifiable_presentation( + vp_template: Template, + vc_list: list[str], + jwk_holder: str, + holder_did: str) + + self.vp = issue_verifiable_presentation( + vp_template: Template, + self.list_credentials, + did.key_material, + did.did) + + if commit: + result = requests.post(self.vp) + return result + + return diff --git a/oidc4vp/templates/credentials_presentation.html b/oidc4vp/templates/credentials_presentation.html new file mode 100644 index 0000000..63f54ba --- /dev/null +++ b/oidc4vp/templates/credentials_presentation.html @@ -0,0 +1,96 @@ +{% extends "idhub/base.html" %} +{% load i18n %} + +{% block content %} +

+ + {{ subtitle }} +

+{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% if form.credentials.all %} +{% for presentation in form.presentation_definition %} +
+
+

{{ presentation|capfirst }} +

+
+ +
+
+
+ + + + + + + + + + + + {% for f in form.credentials.all %} + {% if f.schema.type.lower == presentation.lower %} + + + + + + + + {% endif %} + {% endfor %} + +
{{ f.type }}{{ f.description }}{{ f.get_issued_on }}
+
+
+
+{% endfor %} + +
+ {% trans 'I read and understood the' %} + {% trans 'data sharing notice' %} +
+ + +{% else %} +
+
+

{% trans 'There are not credentials for present' %}

+
+
+{% endif %} +
+ + + +{% endblock %} diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index c4ac0e3..35464b3 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -4,6 +4,7 @@ import didkit import json import jinja2 from django.template.backends.django import Template +from django.template.loader import get_template def generate_did_controller_key(): @@ -49,34 +50,35 @@ def render_and_sign_credential(vc_template: jinja2.Template, jwk_issuer, vc_data def sign_credential(unsigned_vc: str, jwk_issuer): """ - Signs the and unsigned credential with the provided key. + Signs the unsigned credential with the provided key. + The credential template must be rendered with all user data. """ async def inner(): - signed_vc = await didkit.issue_credential( - unsigned_vc, - '{"proofFormat": "ldp"}', - jwk_issuer - ) - return signed_vc + signed_vc = await didkit.issue_credential( + unsigned_vc, + '{"proofFormat": "ldp"}', + jwk_issuer + ) + return signed_vc return asyncio.run(inner()) -def verify_credential(vc, proof_options): +def verify_credential(vc): """ Returns a (bool, str) tuple indicating whether the credential is valid. If the boolean is true, the credential is valid and the second argument can be ignored. If it is false, the VC is invalid and the second argument contains a JSON object with further information. """ async def inner(): - return didkit.verify_credential(vc, proof_options) + return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') return asyncio.run(inner()) -def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: +def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: async def inner(): - unsigned_vp = unsigned_vp_template.render(data) + unsigned_vp = vp_template.render(data) signed_vp = await didkit.issue_presentation( unsigned_vp, '{"proofFormat": "ldp"}', @@ -84,12 +86,6 @@ def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_di ) return signed_vp - # TODO: convert from Jinja2 -> django-templates - env = Environment( - loader=FileSystemLoader("vc_templates"), - autoescape=select_autoescape() - ) - unsigned_vp_template = env.get_template("verifiable_presentation.json") data = { "holder_did": holder_did, "verifiable_credential_list": "[" + ",".join(vc_list) + "]" @@ -106,6 +102,7 @@ def verify_presentation(vp): """ async def inner(): proof_options = '{"proofFormat": "ldp"}' - return didkit.verify_presentation(vp, proof_options) + return await didkit.verify_presentation(vp, proof_options) return asyncio.run(inner()) + From 501d2b2894259c67d8d4f3926dc4728dc8c8d2b6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 09:51:08 +0100 Subject: [PATCH 32/63] send a verificable presentation --- idhub/migrations/0001_initial.py | 3 ++- idhub/models.py | 3 ++- idhub/user/forms.py | 2 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/forms.py | 21 ++++++++++++--------- oidc4vp/models.py | 2 +- oidc4vp/views.py | 12 ++++++++++++ 7 files changed, 31 insertions(+), 14 deletions(-) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 0a49195..d841426 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 18:29 +# Generated by Django 4.2.5 on 2023-12-04 08:44 from django.conf import settings from django.db import migrations, models @@ -181,6 +181,7 @@ class Migration(migrations.Migration): ( 'subject_did', models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subject_credentials', to='idhub.did', diff --git a/idhub/models.py b/idhub/models.py index 915929c..b7c26fa 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -478,6 +478,7 @@ class VerificableCredential(models.Model): DID, on_delete=models.CASCADE, related_name='subject_credentials', + null=True ) issuer_did = models.ForeignKey( DID, @@ -528,7 +529,7 @@ class VerificableCredential(models.Model): context = { 'vc_id': self.id, 'issuer_did': self.issuer_did.did, - 'subject_did': self.subject_did, + 'subject_did': self.subject_did.did, 'issuance_date': issuance_date, 'first_name': self.user.first_name, 'last_name': self.user.last_name, diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 16151bb..5b6f9ab 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -44,7 +44,7 @@ class RequestCredentialForm(forms.Form): if not all([cred.exists(), did.exists()]): return - did = did[0].did + did = did[0] cred = cred[0] try: cred.issue(did) diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 741a8b9..3ee6d7a 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 18:29 +# Generated by Django 4.2.5 on 2023-12-04 08:44 from django.db import migrations, models diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 3d4b16f..18ca7ac 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,5 +1,7 @@ +import requests from django import forms from django.conf import settings +from django.template.loader import get_template from utils.idhub_ssikit import issue_verifiable_presentation from oidc4vp.models import Organization @@ -10,6 +12,7 @@ class AuthorizeForm(forms.Form): def __init__(self, *args, **kwargs): self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) + self.org = kwargs.pop('org', None) self.presentation_definition = kwargs.pop('presentation_definition', []) reg = r'({})'.format('|'.join(self.presentation_definition)) @@ -42,22 +45,22 @@ class AuthorizeForm(forms.Form): return did = self.list_credentials[0].subject_did + vp_template = get_template('credentials/verifiable_presentation.json') + + # self.vp = issue_verifiable_presentation( + # vp_template: Template, + # vc_list: list[str], + # jwk_holder: str, + # holder_did: str) self.vp = issue_verifiable_presentation( - vp_template: Template, - vc_list: list[str], - jwk_holder: str, - holder_did: str) - - self.vp = issue_verifiable_presentation( - vp_template: Template, + vp_template, self.list_credentials, did.key_material, did.did) if commit: - result = requests.post(self.vp) - return result + return org.send(self.vp) return diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ee4e44a..5dae13e 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -70,7 +70,7 @@ class Organization(models.Model): url = "{url}/verify".format( url=self.response_uri.strip("/"), ) - auth = (self.my_client_id, self.client_secret) + auth = (self.my_client_id, self.my_client_secret) return requests.post(url, data=vp, auth=auth) def demand_authorization(self): diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 48ea8b7..5a28741 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -38,6 +38,7 @@ class AuthorizeView(UserView, FormView): vps = self.request.GET.get('presentation_definition') # import pdb; pdb.set_trace() kwargs['presentation_definition'] = json.loads(vps) + kwargs["org"] = self.get_org() return kwargs def form_valid(self, form): @@ -48,6 +49,17 @@ class AuthorizeView(UserView, FormView): messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) + def get_org(self): + client_id = self.request.GET.get("client_id") + if not client_id: + raise Http404("Organization not found!") + + org = get_object_or_404( + Organization, + client_id=client_id, + ) + return org + class VerifyView(View): def get(self, request, *args, **kwargs): From b7dfb6dcfbad30050f5cfa974a1a99492fe52a78 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 10:56:22 +0100 Subject: [PATCH 33/63] send vp_token --- idhub/models.py | 2 +- oidc4vp/forms.py | 40 +++++++++++-------- oidc4vp/models.py | 2 +- .../credentials/verifiable_presentation.json | 2 +- utils/idhub_ssikit/__init__.py | 12 ++++++ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index b7c26fa..ee7ebed 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -529,7 +529,7 @@ class VerificableCredential(models.Model): context = { 'vc_id': self.id, 'issuer_did': self.issuer_did.did, - 'subject_did': self.subject_did.did, + 'subject_did': self.subject_did and self.subject_did.did or '', 'issuance_date': issuance_date, 'first_name': self.user.first_name, 'last_name': self.user.last_name, diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 18ca7ac..7c04255 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,9 +1,13 @@ +import json import requests + from django import forms from django.conf import settings from django.template.loader import get_template +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError -from utils.idhub_ssikit import issue_verifiable_presentation +from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization @@ -33,34 +37,36 @@ class AuthorizeForm(forms.Form): ) def clean(self): data = super().clean() - import pdb; pdb.set_trace() self.list_credentials = [] for c in self.credentials: if str(c.id) == data.get(c.schema.type.lower()): + if c.status is not c.Status.ISSUED.value or not c.data: + txt = _('There are some problems with this credentials') + raise ValidationError(txt) + self.list_credentials.append(c) + return data def save(self, commit=True): if not self.list_credentials: return - did = self.list_credentials[0].subject_did - vp_template = get_template('credentials/verifiable_presentation.json') - - # self.vp = issue_verifiable_presentation( - # vp_template: Template, - # vc_list: list[str], - # jwk_holder: str, - # holder_did: str) - - self.vp = issue_verifiable_presentation( - vp_template, - self.list_credentials, - did.key_material, - did.did) + self.get_verificable_presentation() if commit: - return org.send(self.vp) + return self.org.send(self.vp) return + def get_verificable_presentation(self): + did = self.list_credentials[0].subject_did + vp_template = get_template('credentials/verifiable_presentation.json') + vc_list = json.dumps([json.loads(x.data) for x in self.list_credentials]) + + context = { + "holder_did": did.did, + "verifiable_credential_list": vc_list + } + unsigned_vp = vp_template.render(context) + self.vp = create_verifiable_presentation(did.key_material, unsigned_vp) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 5dae13e..4111c18 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -71,13 +71,13 @@ class Organization(models.Model): url=self.response_uri.strip("/"), ) auth = (self.my_client_id, self.my_client_secret) + # import pdb; pdb.set_trace() return requests.post(url, data=vp, auth=auth) def demand_authorization(self): """ Send the a request for start a process of Verifier """ - # import pdb; pdb.set_trace() url = "{url}/verify?demand_uri={redirect_uri}".format( url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI diff --git a/oidc4vp/templates/credentials/verifiable_presentation.json b/oidc4vp/templates/credentials/verifiable_presentation.json index 752affb..a55b769 100644 --- a/oidc4vp/templates/credentials/verifiable_presentation.json +++ b/oidc4vp/templates/credentials/verifiable_presentation.json @@ -7,5 +7,5 @@ "VerifiablePresentation" ], "holder": "{{ holder_did }}", - "verifiableCredential": {{ verifiable_credential_list }} + "verifiableCredential": {{ verifiable_credential_list|safe }} } diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 35464b3..cc3e9b4 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -94,6 +94,18 @@ def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk return asyncio.run(inner()) +def create_verifiable_presentation(jwk_holder: str, unsigned_vp: str) -> str: + async def inner(): + signed_vp = await didkit.issue_presentation( + unsigned_vp, + '{"proofFormat": "ldp"}', + jwk_holder + ) + return signed_vp + + return asyncio.run(inner()) + + def verify_presentation(vp): """ Returns a (bool, str) tuple indicating whether the credential is valid. From e511393fee57cb36de65a3a154c98431da1497c6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 15:47:48 +0100 Subject: [PATCH 34/63] verify --- oidc4vp/models.py | 4 +-- oidc4vp/views.py | 63 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 4111c18..55fb327 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -71,8 +71,8 @@ class Organization(models.Model): url=self.response_uri.strip("/"), ) auth = (self.my_client_id, self.my_client_secret) - # import pdb; pdb.set_trace() - return requests.post(url, data=vp, auth=auth) + data = {"vp_token": vp} + return requests.post(url, data=data, auth=auth) def demand_authorization(self): """ diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 5a28741..77598b4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -5,6 +5,8 @@ from django.conf import settings from django.views.generic.edit import View, FormView from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.contrib import messages @@ -13,12 +15,12 @@ from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView from oidc4vp.forms import AuthorizeForm +from utils.idhub_ssikit import verify_presentation # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect -# from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest # from more_itertools import flatten, unique_everseen @@ -43,10 +45,29 @@ class AuthorizeView(UserView, FormView): def form_valid(self, form): authorization = form.save() - if authorization: - return redirect(authorization) - else: + import pdb; pdb.set_trace() + if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + try: + authorization = json.loads(authorization.text) + except: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + + verify = authorization.get('verify') + result, msg = verify.split(",") + if 'error' in result.lower(): + messages.error(self.request, msg) + if 'ok' in result.lower(): + messages.success(self.request, msg) + + if authorization.get('redirect_uri'): + return redirect(authorization.get('redirect_uri')) + elif authorization.get('response'): + txt = authorization.get('response') + messages.success(self.request, txt) + return super().form_valid(form) def get_org(self): @@ -61,6 +82,7 @@ class AuthorizeView(UserView, FormView): return org +@method_decorator(csrf_exempt, name='dispatch') class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) @@ -73,6 +95,7 @@ class VerifyView(View): return HttpResponse(res) def validate(self, request): + # import pdb; pdb.set_trace() auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() @@ -81,17 +104,37 @@ class VerifyView(View): client_id, client_secret = decoded_auth.split(':', 1) org_url = request.GET.get('demand_uri') org = get_object_or_404( - Organization, - response_uri=org_url, - client_id=client_id, - client_secret=client_secret - ) + Organization, + client_id=client_id, + client_secret=client_secret + ) return org - raise Http404("Organization not found!") + raise Http404("Page not Found!") def post(self, request, *args, **kwargs): org = self.validate(request) + vp_token = self.request.POST.get("vp_token") + if not vp_token: + raise Http404("Page not Found!") + + response = self.get_response_verify() + result = verify_presentation(request.POST["vp_token"]) + verification = json.loads(result) + if verification.get('errors') or verification.get('warnings'): + response["verify"] = "Error, Verification Failed" + return HttpResponse(response) + + response["verify"] = "Ok, Verification correct" + response["response"] = "Validation Code 255255255" + return HttpResponse(json.dumps(response)) + + def get_response_verify(self): + return { + "verify": ',', + "redirect_uri": "", + "response": "", + } # import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) From 39b89bebe0c7d04b7b0c8ca2ab62aec0ede4e408 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 17:12:12 +0100 Subject: [PATCH 35/63] fix json request --- oidc4vp/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 77598b4..fef0f50 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -3,7 +3,7 @@ import base64 from django.conf import settings from django.views.generic.edit import View, FormView -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator @@ -45,12 +45,11 @@ class AuthorizeView(UserView, FormView): def form_valid(self, form): authorization = form.save() - import pdb; pdb.set_trace() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) try: - authorization = json.loads(authorization.text) + authorization = authorization.json() except: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) @@ -127,7 +126,7 @@ class VerifyView(View): response["verify"] = "Ok, Verification correct" response["response"] = "Validation Code 255255255" - return HttpResponse(json.dumps(response)) + return JsonResponse(response) def get_response_verify(self): return { From 753e1f6d1a1929c91f7309c1ef79202d4171e593 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 17:12:39 +0100 Subject: [PATCH 36/63] promotion page --- promotion/__init__.py | 0 promotion/admin.py | 3 + promotion/apps.py | 6 + promotion/forms.py | 59 + promotion/migrations/__init__.py | 0 promotion/models.py | 3 + promotion/templates/select_wallet.html | 1100 +++++++++++++++ .../templates/somconnexio.tarifes-mobil.html | 1218 +++++++++++++++++ promotion/tests.py | 3 + promotion/urls.py | 14 + promotion/views.py | 30 + trustchain_idhub/settings.py | 3 +- trustchain_idhub/urls.py | 1 + 13 files changed, 2439 insertions(+), 1 deletion(-) create mode 100644 promotion/__init__.py create mode 100644 promotion/admin.py create mode 100644 promotion/apps.py create mode 100644 promotion/forms.py create mode 100644 promotion/migrations/__init__.py create mode 100644 promotion/models.py create mode 100644 promotion/templates/select_wallet.html create mode 100644 promotion/templates/somconnexio.tarifes-mobil.html create mode 100644 promotion/tests.py create mode 100644 promotion/urls.py create mode 100644 promotion/views.py diff --git a/promotion/__init__.py b/promotion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/promotion/admin.py b/promotion/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/promotion/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/promotion/apps.py b/promotion/apps.py new file mode 100644 index 0000000..baccc02 --- /dev/null +++ b/promotion/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PromotionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'promotion' diff --git a/promotion/forms.py b/promotion/forms.py new file mode 100644 index 0000000..a6f6d78 --- /dev/null +++ b/promotion/forms.py @@ -0,0 +1,59 @@ + +import json +import requests + +from django import forms +from django.conf import settings +from django.template.loader import get_template +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + +from utils.idhub_ssikit import create_verifiable_presentation +from oidc4vp.models import Organization + + +class WalletForm(forms.Form): + + def __init__(self, *args, **kwargs): + self.presentation_definition = kwargs.pop('presentation_definition', []) + + reg = r'({})'.format('|'.join(self.presentation_definition)) + + self.credentials = self.user.vcredentials.filter( + schema__type__iregex=reg + ) + super().__init__(*args, **kwargs) + for vp in self.presentation_definition: + vp = vp.lower() + choices = [ + (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( + schema__type__iexact=vp) + ] + self.fields[vp.lower()] = forms.ChoiceField( + widget=forms.RadioSelect, + choices=choices + ) + def clean(self): + data = super().clean() + self.list_credentials = [] + for c in self.credentials: + if str(c.id) == data.get(c.schema.type.lower()): + if c.status is not c.Status.ISSUED.value or not c.data: + txt = _('There are some problems with this credentials') + raise ValidationError(txt) + + self.list_credentials.append(c) + + return data + + def save(self, commit=True): + if not self.list_credentials: + return + + self.get_verificable_presentation() + + if commit: + return self.org.send(self.vp) + + return + diff --git a/promotion/migrations/__init__.py b/promotion/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/promotion/models.py b/promotion/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/promotion/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/promotion/templates/select_wallet.html b/promotion/templates/select_wallet.html new file mode 100644 index 0000000..254b0fb --- /dev/null +++ b/promotion/templates/select_wallet.html @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + + + +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/templates/somconnexio.tarifes-mobil.html b/promotion/templates/somconnexio.tarifes-mobil.html new file mode 100644 index 0000000..e65b856 --- /dev/null +++ b/promotion/templates/somconnexio.tarifes-mobil.html @@ -0,0 +1,1218 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ +
+
El més preguntat
+
    +
  • + Mòbil: tarifes i detalls +
    +

    Pots consultar els preus i detalls de les tarifes de mòbil en aquest enllaç.

    +

     

    +
    +
  • +
  • + Quina cobertura de mòbil té Som Connexió? +
    +

    La cobertura del servei és Yoigo/MásMóvil, Orange (4G) i Movistar (3G) i el nostre proveïdor de serveis és MásMóvil.

    +
    +
  • +
  • + Trucades gratuïtes entre mòbils de Som Connexió +
    +

    Les tarifes de mòbil de 0 minuts i 150 minuts inclouen 1.000 minuts de trucades gratuïtes a d’altres telèfons mòbils de Som Connexió (a la tarifa de minuts il·limitats ja estan incloses totes les trucades a mòbils de qualsevol operadora dins l’estat espanyol).

    +

    Això vol dir que si truques a un telèfon mòbil de Som Connexió, els minuts que utilitzis no es descomptaran del teu abonament sinó d’aquests 1.000 minuts.

    +

    Aquesta prestació només s’aplica a la telefonia mòbil de Som Connexió. La telefonia fixa de Som Connexió no inclou aquests minuts, ni en el servei de Fibra ni en el d’ADSL.

    +

    Aquest servei és automàtic. No s’ha de fer res per activar-lo i no es pot desactivar.

    +
    +
  • +
  • + Costos d’alta (mòbil) +
    +

    Quan es fa una alta de servei de telefonia mòbil, afegim a la factura els costos de l’alta del servei: 2,05€ (IVA inclòs)

    +
    +
  • +
  • + Puc mantenir el meu número de mòbil? +
    +

    Sí, sense cap dubte! Se’n diu portabilitat. Però si vols, també es pot canviar.

    +
    +
  • +
  • + Contractar dades addicionals +
    +

    Si esgotes dades abans d’acabar el mes, tens l’opció de contractar dades addicionals que podràs utilitzar fins a acabar el mes:

    +
      +
    • 500 MB (2 €)*
    • +
    • 1 GB (3 €)*
    • +
    • 3 GB (5 €)*
    • +
    • 5 GB (6 €)*
    • +
    • 10 GB (9€)*
    • +
    +

    * IVA inclòs

    +

    Aquí t’expliquem com afegir dades extra.

    +

    Contractar dades addicionals en les tarifes compartides

    +

    Si els mòbils que estan compartint dades esgoten els 50 GB abans que acabi el més, es pot comprar una ampliació de 10 GB per 9€ o de 20 GB per 12€ que podran consumir els diferents mòbils. Aquí t’expliquem com afegir dades extra a les tarifes compartides.

    +
    +
  • +
  • + Preus SMS +
    +

    Els SMS tenen un preu de 0,10€/sms

    +

    IMPORTANT: els SMS no estan inclosos a cap de les tarifes de Som Connexió. Tampoc a la tarifa de minuts il·limitats.

    +
    +
  • +
  • + Preus de trucades fora de tarifa des del mòbil +
    +

    Aquests són els preus de les trucades un cop has superat els minuts que tens contractats a la teva tarifa:

    +

    0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa.

    +

    A partir del minut 6, es paga 0,036€/min.

    +

    Consulta les tarifes de roaming AQUÍ.

    +
    +
  • +
  • + Tarifa bàsica mòbil: 0 min i 0 Gb +
    +

    Amb un cost base de 2€ mensuals, es tracta d’un consum “a granel”, és a dir, que es paga per minut i per unitat de dades consumides.

    +

    Aquesta tarifa és adequada per a persones que utilitzen el mòbil per estar localitzables i fer una trucada molt puntualment (només surt a compte si es fan menys de 6 trucades al mes de curta durada) i no utilitza el mòbil per connectar-se a Internet o ho fa de una manera molt esporàdica.

    +

    El preu per trucada en aquest abonament és de 0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa. A partir del minut 6, es paga 0,036€/min

    +

    El preu del servei de dades quan no s’ha contractat cap abonament és de 0,036€/Mb

    +

    Si vols desactivar el servei de dades, ens ho has de sol·licitar a serveis@somconnexio.coop.

    +
    +
  • +
  • + NÚMEROS DE TARIFACIÓ ESPECIAL +
    +

    Els preus estàndard de veu o missatges exclouen:

    +
      +
    • Els serveis de tarifació especial (per exemple: 80x, 90x o SMS Premium). Mira la taula de tarifes AQUÍ
    • +
    • Trànsit internacional
    • +
    • Trànsit en itinerància
    • +
    +

    Alguns dels serveis de tarifació especial de cost elevat estan bloquejats per defecte. Per tenir més informació sobre l’opció de bloqueig dels números de cost especial, consulta aquesta TAULA.

    +

    En aquest enllaç de la OCU ens donen més informació sobre els números de tarificació especial.

    +
    +
  • +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/tests.py b/promotion/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/promotion/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/promotion/urls.py b/promotion/urls.py new file mode 100644 index 0000000..8fd5eb7 --- /dev/null +++ b/promotion/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, reverse_lazy + +from promotion import views + + +app_name = 'promotion' + + +urlpatterns = [ + path('', views.PromotionView.as_view(), + name="show_promotion"), + path('select_wallet', views.SelectWalletView.as_view(), + name="select_wallet"), +] diff --git a/promotion/views.py b/promotion/views.py new file mode 100644 index 0000000..6988ac0 --- /dev/null +++ b/promotion/views.py @@ -0,0 +1,30 @@ +from django.views.generic.edit import View, FormView +from django.template.loader import get_template +from django.urls import reverse_lazy +from django.http import HttpResponse + +from promotion.forms import WalletForm + + +class PromotionView(View): + template_name = "somconnexio.tarifes-mobil.html" + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + + +class SelectWalletView(FormView): + template_name = "select_wallet.html" + form_class = WalletForm + success_url = reverse_lazy('promotion:select_wallet') + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + # context + ).render() + return HttpResponse(template) + diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index e25b340..274416d 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -73,7 +73,8 @@ INSTALLED_APPS = [ 'django_tables2', 'idhub_auth', 'oidc4vp', - 'idhub' + 'idhub', + 'promotion' ] MIDDLEWARE = [ diff --git a/trustchain_idhub/urls.py b/trustchain_idhub/urls.py index 1668872..e8468a3 100644 --- a/trustchain_idhub/urls.py +++ b/trustchain_idhub/urls.py @@ -25,4 +25,5 @@ urlpatterns = [ # path('django-admin/', admin.site.urls), path('', include('idhub.urls')), path('oidc4vp/', include('oidc4vp.urls')), + path('promotion/', include('promotion.urls')), ] From 914d408dedca6accdd6497454f7ae0eb1e578a1f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 7 Dec 2023 18:10:04 +0100 Subject: [PATCH 37/63] allow code --- oidc4vp/forms.py | 11 +- oidc4vp/models.py | 95 +++++++++------ oidc4vp/views.py | 112 +++++++----------- promotion/forms.py | 70 +++++------ promotion/models.py | 25 +++- promotion/templates/select_wallet.html | 26 ++++ promotion/templates/somconnexio_contract.html | 1 + promotion/views.py | 81 +++++++++++-- 8 files changed, 277 insertions(+), 144 deletions(-) create mode 100644 promotion/templates/somconnexio_contract.html diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 7c04255..5f6f6df 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization +from idhub.models import VerificableCredential class AuthorizeForm(forms.Form): @@ -17,12 +18,14 @@ class AuthorizeForm(forms.Form): self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.org = kwargs.pop('org', None) + self.code = kwargs.pop('code', None) self.presentation_definition = kwargs.pop('presentation_definition', []) reg = r'({})'.format('|'.join(self.presentation_definition)) self.credentials = self.user.vcredentials.filter( - schema__type__iregex=reg + schema__type__iregex=reg, + status=VerificableCredential.Status.ISSUED.value ) super().__init__(*args, **kwargs) for vp in self.presentation_definition: @@ -46,6 +49,10 @@ class AuthorizeForm(forms.Form): self.list_credentials.append(c) + if not self.code: + txt = _("There isn't code in request") + raise ValidationError(txt) + return data def save(self, commit=True): @@ -55,7 +62,7 @@ class AuthorizeForm(forms.Form): self.get_verificable_presentation() if commit: - return self.org.send(self.vp) + return self.org.send(self.vp, self.code) return diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 55fb327..b29349c 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -4,8 +4,10 @@ import secrets from django.conf import settings from django.http import QueryDict from django.utils.translation import gettext_lazy as _ +from django.shortcuts import get_object_or_404 from idhub_auth.models import User from django.db import models +from utils.idhub_ssikit import verify_presentation SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -63,7 +65,7 @@ class Organization(models.Model): max_length=250 ) - def send(self, vp): + def send(self, vp, code): """ Send the verificable presentation to Verifier """ @@ -72,6 +74,9 @@ class Organization(models.Model): ) auth = (self.my_client_id, self.my_client_secret) data = {"vp_token": vp} + if code: + data["code"] = code + return requests.post(url, data=data, auth=auth) def demand_authorization(self): @@ -100,13 +105,8 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ - # nonce = models.CharField(max_length=50) - # expected_credentials = models.CharField(max_length=255) - # expected_contents = models.TextField() - # action = models.TextField() - # response_or_redirect = models.CharField(max_length=255) - code = models.CharField(max_length=24, default=set_code) + code_used = models.BooleanField() created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( @@ -121,19 +121,24 @@ class Authorization(models.Model): null=True, ) - def authorize(self): + def authorize(self, path=None): data = { "response_type": "vp_token", "response_mode": "direct_post", "client_id": self.organization.my_client_id, "presentation_definition": self.presentation_definition, + "code": self.code, "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) query_dict.update(data) + response_uri = self.organization.response_uri.strip("/") + if path: + response_uri = "{}/{}".format(response_uri, path.strip("/")) + url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri.strip("/"), + response_uri=response_uri, params=query_dict.urlencode() ) return url @@ -145,9 +150,8 @@ class OAuth2VPToken(models.Model): and the result of verify. """ created = models.DateTimeField(auto_now=True) - code = models.CharField(max_length=250) - result_verify = models.BooleanField(max_length=250) - presentation_definition = models.CharField(max_length=250) + result_verify = models.CharField(max_length=255) + vp_token = models.TextField() organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -163,31 +167,54 @@ class OAuth2VPToken(models.Model): authorization = models.ForeignKey( Authorization, on_delete=models.SET_NULL, + related_name='oauth2vptoken', null=True, ) + def __init__(self, *args, **kwargs): + code = kwargs.pop("code", None) + super().__init__(*args, **kwargs) + + self.authorization = get_object_or_404( + Authorization, + code=code + ) + def verifing(self): - pass + self.result_verify = verify_presentation(self.vp_token) + def get_response_verify(self): + response = { + "verify": ',', + "redirect_uri": "", + "response": "", + } + verification = json.loads(self.result_verify) + if verification.get('errors') or verification.get('warnings'): + response["verify"] = "Error, Verification Failed" + return response + + response["verify"] = "Ok, Verification correct" + response["redirect_uri"] = self.get_redirect_url() + return response -class VPVerifyRequest(models.Model): - """ - `nonce` is an opaque random string used to lookup verification requests. URL-safe. - Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" - `expected_credentials` is a JSON list of credential types that must be present in this VP. - Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] - `expected_contents` is a JSON object that places optional constraints on the contents of the - returned VP. - Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] - `action` is (for now) a JSON object describing the next steps to take if this verification - is successful. For example "send mail to with and " - Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} - `response` is a URL that the user's wallet will redirect the user to. - `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification - """ - nonce = models.CharField(max_length=50) - expected_credentials = models.CharField(max_length=255) - expected_contents = models.TextField() - action = models.TextField() - response_or_redirect = models.CharField(max_length=255) - submitted_on = models.DateTimeField(auto_now=True) + def get_redirect_url(self): + data = { + "code": self.authorization.code, + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + response_uri = settings.ALLOW_CODE_URI + + url = '{response_uri}?{params}'.format( + response_uri=response_uri, + params=query_dict.urlencode() + ) + return url + + def get_user_info(self): + tk = json.loads(self.vp_token) + self.user_info = tk.get( + "verifiableCredential", [{}] + )[-1].get("credentialSubject") diff --git a/oidc4vp/views.py b/oidc4vp/views.py index fef0f50..dbdac7d 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -18,13 +18,6 @@ from oidc4vp.forms import AuthorizeForm from utils.idhub_ssikit import verify_presentation -# from django.core.mail import send_mail -# from django.http import HttpResponse, HttpResponseRedirect - -# from oidc4vp.models import VPVerifyRequest -# from more_itertools import flatten, unique_everseen - - class AuthorizeView(UserView, FormView): title = _("My wallet") section = "MyWallet" @@ -37,10 +30,14 @@ class AuthorizeView(UserView, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user - vps = self.request.GET.get('presentation_definition') + try: + vps = json.loads(self.request.GET.get('presentation_definition')) + except: + vps = [] # import pdb; pdb.set_trace() - kwargs['presentation_definition'] = json.loads(vps) + kwargs['presentation_definition'] = vps kwargs["org"] = self.get_org() + kwargs["code"] = self.request.GET.get('code') return kwargs def form_valid(self, form): @@ -90,11 +87,35 @@ class VerifyView(View): organization=org, presentation_definition=presentation_definition ) + authorization.save() res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) - def validate(self, request): + def post(self, request, *args, **kwargs): # import pdb; pdb.set_trace() + code = self.request.POST.get("code") + vp_tk = self.request.POST.get("vp_token") + + if not vp_tk or not code: + raise Http404("Page not Found!") + + org = self.validate(request) + + vp_token = OAuth2VPToken( + vp_token = vp_tk, + organization=org, + code=code + ) + + vp_token.verifing() + response = vp_token.get_response_verify() + vp_token.save() + if response["redirect_uri"]: + response["response"] = "Validation Code 255255255" + + return JsonResponse(response) + + def validate(self, request): auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() @@ -111,62 +132,21 @@ class VerifyView(View): raise Http404("Page not Found!") - def post(self, request, *args, **kwargs): - org = self.validate(request) - vp_token = self.request.POST.get("vp_token") - if not vp_token: + +class AllowCodeView(View): + def get(self, request, *args, **kwargs): + code = self.request.GET.get("code") + + if not code: + raise Http404("Page not Found!") + self.authorization = get_object_or_404( + Authorization, + code=code, + code_used=False + ) + if not self.authorization.promotions: raise Http404("Page not Found!") - response = self.get_response_verify() - result = verify_presentation(request.POST["vp_token"]) - verification = json.loads(result) - if verification.get('errors') or verification.get('warnings'): - response["verify"] = "Error, Verification Failed" - return HttpResponse(response) - - response["verify"] = "Ok, Verification correct" - response["response"] = "Validation Code 255255255" - return JsonResponse(response) + promotion = self.authorization.promotions[0] + return redirect(promotion.get_url(code)) - def get_response_verify(self): - return { - "verify": ',', - "redirect_uri": "", - "response": "", - } - # import pdb; pdb.set_trace() - # # TODO: incorporate request.POST["presentation_submission"] as schema definition - # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) - # if not presentation_valid: - # raise Exception("Failed to verify signature on the given Verifiable Presentation.") - # vp = json.loads(request.POST["vp_token"]) - # nonce = vp["nonce"] - # # "vr" = verification_request - # vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 - # # Get a list of all included verifiable credential types - # included_credential_types = unique_everseen(flatten([ - # vc["type"] for vc in vp["verifiableCredential"] - # ])) - # # Check that it matches what we requested - # for requested_vc_type in json.loads(vr.expected_credentials): - # if requested_vc_type not in included_credential_types: - # raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error - # # Perform whatever action we have to do - # action = json.loads(vr.action) - # if action["action"] == "send_mail": - # subject = action["params"]["subject"] - # to_email = action["params"]["to"] - # from_email = "noreply@verifier-portal" - # body = request.POST["vp-token"] - # send_mail( - # subject, - # body, - # from_email, - # [to_email] - # ) - # elif action["action"] == "something-else": - # pass - # else: - # raise Exception("Unknown action!") - # # OK! Your verifiable presentation was successfully presented. - # return HttpResponseRedirect(vr.response_or_redirect) diff --git a/promotion/forms.py b/promotion/forms.py index a6f6d78..d7af389 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -9,51 +9,55 @@ from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation -from oidc4vp.models import Organization +from oidc4vp.models import Organization, Authorization class WalletForm(forms.Form): + organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): self.presentation_definition = kwargs.pop('presentation_definition', []) - - reg = r'({})'.format('|'.join(self.presentation_definition)) - - self.credentials = self.user.vcredentials.filter( - schema__type__iregex=reg - ) super().__init__(*args, **kwargs) - for vp in self.presentation_definition: - vp = vp.lower() - choices = [ - (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( - schema__type__iexact=vp) - ] - self.fields[vp.lower()] = forms.ChoiceField( - widget=forms.RadioSelect, - choices=choices - ) - def clean(self): - data = super().clean() - self.list_credentials = [] - for c in self.credentials: - if str(c.id) == data.get(c.schema.type.lower()): - if c.status is not c.Status.ISSUED.value or not c.data: - txt = _('There are some problems with this credentials') - raise ValidationError(txt) - - self.list_credentials.append(c) - - return data + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] def save(self, commit=True): - if not self.list_credentials: + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): return - self.get_verificable_presentation() + self.org = self.org[0] + + self.authorization = Authorization( + organization=self.org, + presentation_definition=self.presentation_definition, + ) + self.promotion = Promotion( + discount = Promotion.Types.VULNERABLE.value, + authorize = self.authorization + ) if commit: - return self.org.send(self.vp) + self.authorization.save() + self.promotion.save() - return + return self.authorization.authorize() + + return + +class ContractForm(forms.Form): + nif = forms.CharField() + name = forms.CharField() + first_last_name = forms.CharField() + second_last_name = forms.CharField() + email = forms.CharField() + email_repeat = forms.CharField() + telephone = forms.CharField() + birthday = forms.CharField() + gen = forms.CharField() + lang = forms.CharField() diff --git a/promotion/models.py b/promotion/models.py index 71a8362..bbf7bbc 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -1,3 +1,26 @@ from django.db import models +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from oidc4vp.models import Authorization -# Create your models here. + +class Promotion(models.Model): + class Types(models.IntegerChoices): + VULNERABLE = 1, _("Financial vulnerability") + + name = models.CharField(max_length=250) + discount = models.PositiveSmallIntegerField( + choices=Types.choices, + ) + authorize = models.ForeignKey( + Authorization, + on_delete=models.CASCADE, + related_name='promotions', + null=True, + ) + + def get_url(self, code): + url = "{}?code={}".format( + reverse_lazy("promotion:show_promotion"), + code + ) \ No newline at end of file diff --git a/promotion/templates/select_wallet.html b/promotion/templates/select_wallet.html index 254b0fb..ba813ee 100644 --- a/promotion/templates/select_wallet.html +++ b/promotion/templates/select_wallet.html @@ -528,6 +528,32 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+{% load i18n %} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +
+
+ {% bootstrap_form form %} +
+
+ + +
diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html new file mode 100644 index 0000000..251e821 --- /dev/null +++ b/promotion/templates/somconnexio_contract.html @@ -0,0 +1 @@ +Som Connexió
diff --git a/promotion/views.py b/promotion/views.py index 6988ac0..dcde3f0 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -1,9 +1,12 @@ +import json + from django.views.generic.edit import View, FormView +from django.shortcuts import redirect from django.template.loader import get_template from django.urls import reverse_lazy from django.http import HttpResponse -from promotion.forms import WalletForm +from promotion.forms import WalletForm, ContractForm class PromotionView(View): @@ -16,15 +19,77 @@ class PromotionView(View): return HttpResponse(template) +class PromotionMobile1View(FormView): + template_name = "somconnexio_contract.html" + promotion = None + vp_tokens = None + authorization = None + form_class = ContractForm + def get(self, request, *args, **kwargs): + code = self.request.GET.get("code") + self.get_discount(code) + self.context = { + "promotion": self.promotion, + "verificable_presentation": self.vp_token + } + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + self.vp_token.get_user_info() + kwargs['verificable_presentation'] = self.vp_token + kwargs["nif"] = self.vp_token.user_info.get("nif", '') + kwargs["name"] = self.vp_token.user_info.get("name", '') + kwargs["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') + kwargs["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') + kwargs["email"] = self.vp_token.user_info.get("email", '') + kwargs["email_repeat"] = self.vp_token.user_info.get("email", '') + kwargs["telephone"] = self.vp_token.user_info.get("telephone", '') + kwargs["birthday"] = self.vp_token.user_info.get("birthday", '') + kwargs["gen"] = self.vp_token.user_info.get("gen", '') + kwargs["lang"] = self.vp_token.user_info.get("lang", '') + return kwargs + + def form_valid(self, form): + url = form.save() + return redirect(url) + + def get_discount(self, code): + self.authorization = Authorization.objects.filter( + code=code, + code_unused=False + ).first() + if self.authorization: + if self.authorization.promotions: + self.promotion = self.authorization.promotionsp[-1] + if self.authorization.vp_tokens: + self.vp_tokens = self.authorization.vp_tokens[-1] + + class SelectWalletView(FormView): template_name = "select_wallet.html" form_class = WalletForm success_url = reverse_lazy('promotion:select_wallet') - def get(self, request, *args, **kwargs): - self.context = {} - template = get_template( - self.template_name, - # context - ).render() - return HttpResponse(template) + # def get(self, request, *args, **kwargs): + # self.context = {'form': fo} + # template = get_template( + # self.template_name, + # # context + # ).render() + # return HttpResponse(template) + + # def post(self, request, *args, **kwargs): + # super().post(request, *args, **kwargs) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['presentation_definition'] = json.dumps(["MemberShipCard"]) + return kwargs + + def form_valid(self, form): + url = form.save() + return redirect(url) From 93557fe2776f5f2dcac46fa6c8ce5ded582bd1b9 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 12:11:18 +0100 Subject: [PATCH 38/63] form of contract --- idhub/migrations/0001_initial.py | 2 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/migrations/0001_initial.py | 29 +- promotion/migrations/0001_initial.py | 45 + promotion/models.py | 7 +- promotion/templates/somconnexio_contract.html | 1172 +++++++++++++++- ...il.html => somconnexio_tarifes_mobil.html} | 0 promotion/templates/somconnexio_thanks.html | 1223 +++++++++++++++++ promotion/urls.py | 4 + promotion/views.py | 49 +- 10 files changed, 2492 insertions(+), 41 deletions(-) create mode 100644 promotion/migrations/0001_initial.py rename promotion/templates/{somconnexio.tarifes-mobil.html => somconnexio_tarifes_mobil.html} (100%) create mode 100644 promotion/templates/somconnexio_thanks.html diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index d841426..6d87ae7 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-04 08:44 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.conf import settings from django.db import migrations, models diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 3ee6d7a..f460a62 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-04 08:44 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.db import migrations, models diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 230817d..700c4e8 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-29 10:18 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.conf import settings from django.db import migrations, models @@ -30,6 +30,7 @@ class Migration(migrations.Migration): 'code', models.CharField(default=oidc4vp.models.set_code, max_length=24), ), + ('code_used', models.BooleanField()), ('created', models.DateTimeField(auto_now=True)), ('presentation_definition', models.CharField(max_length=250)), ], @@ -70,26 +71,6 @@ class Migration(migrations.Migration): ), ], ), - migrations.CreateModel( - name='VPVerifyRequest', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('nonce', models.CharField(max_length=50)), - ('expected_credentials', models.CharField(max_length=255)), - ('expected_contents', models.TextField()), - ('action', models.TextField()), - ('response_or_redirect', models.CharField(max_length=255)), - ('submitted_on', models.DateTimeField(auto_now=True)), - ], - ), migrations.CreateModel( name='OAuth2VPToken', fields=[ @@ -103,14 +84,14 @@ class Migration(migrations.Migration): ), ), ('created', models.DateTimeField(auto_now=True)), - ('code', models.CharField(max_length=250)), - ('result_verify', models.BooleanField(max_length=250)), - ('presentation_definition', models.CharField(max_length=250)), + ('result_verify', models.CharField(max_length=255)), + ('vp_token', models.TextField()), ( 'authorization', models.ForeignKey( null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='oauth2vptoken', to='oidc4vp.authorization', ), ), diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py new file mode 100644 index 0000000..cbc1f17 --- /dev/null +++ b/promotion/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.5 on 2023-12-11 08:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ('oidc4vp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Promotion', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('name', models.CharField(max_length=250)), + ( + 'discount', + models.PositiveSmallIntegerField( + choices=[(1, 'Financial vulnerability')] + ), + ), + ( + 'authorize', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='promotions', + to='oidc4vp.authorization', + ), + ), + ], + ), + ] diff --git a/promotion/models.py b/promotion/models.py index bbf7bbc..2750b79 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -21,6 +21,9 @@ class Promotion(models.Model): def get_url(self, code): url = "{}?code={}".format( - reverse_lazy("promotion:show_promotion"), + reverse_lazy("promotion:contract"), code - ) \ No newline at end of file + ) + + def get_discount(self, price): + return price - price*0.25 \ No newline at end of file diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 251e821..71d0cb2 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -1 +1,1171 @@ -Som Connexió
+ + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+

HOLA HOLA

+ + +
+
+{% load i18n %} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} + + +
+
+
+ +
+
    + + Resum + +
  • + Imports inicials +
  • +
  • + Tarjeta SIM {{ sim }}€ +
  • +
  • + Factura mensual +
  • +
  • + {{ mensual }}€ +
  • + Total {{ total }}€ (IVA inclòs) +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/templates/somconnexio.tarifes-mobil.html b/promotion/templates/somconnexio_tarifes_mobil.html similarity index 100% rename from promotion/templates/somconnexio.tarifes-mobil.html rename to promotion/templates/somconnexio_tarifes_mobil.html diff --git a/promotion/templates/somconnexio_thanks.html b/promotion/templates/somconnexio_thanks.html new file mode 100644 index 0000000..b3c694f --- /dev/null +++ b/promotion/templates/somconnexio_thanks.html @@ -0,0 +1,1223 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+

Contrato

+ + +
+
+Contrato realizado correctamente. Te hemos enviado la información por email. +
+
+ +
+
El més preguntat
+
    +
  • + Mòbil: tarifes i detalls +
    +

    Pots consultar els preus i detalls de les tarifes de mòbil en aquest enllaç.

    +

     

    +
    +
  • +
  • + Quina cobertura de mòbil té Som Connexió? +
    +

    La cobertura del servei és Yoigo/MásMóvil, Orange (4G) i Movistar (3G) i el nostre proveïdor de serveis és MásMóvil.

    +
    +
  • +
  • + Trucades gratuïtes entre mòbils de Som Connexió +
    +

    Les tarifes de mòbil de 0 minuts i 150 minuts inclouen 1.000 minuts de trucades gratuïtes a d’altres telèfons mòbils de Som Connexió (a la tarifa de minuts il·limitats ja estan incloses totes les trucades a mòbils de qualsevol operadora dins l’estat espanyol).

    +

    Això vol dir que si truques a un telèfon mòbil de Som Connexió, els minuts que utilitzis no es descomptaran del teu abonament sinó d’aquests 1.000 minuts.

    +

    Aquesta prestació només s’aplica a la telefonia mòbil de Som Connexió. La telefonia fixa de Som Connexió no inclou aquests minuts, ni en el servei de Fibra ni en el d’ADSL.

    +

    Aquest servei és automàtic. No s’ha de fer res per activar-lo i no es pot desactivar.

    +
    +
  • +
  • + Costos d’alta (mòbil) +
    +

    Quan es fa una alta de servei de telefonia mòbil, afegim a la factura els costos de l’alta del servei: 2,05€ (IVA inclòs)

    +
    +
  • +
  • + Puc mantenir el meu número de mòbil? +
    +

    Sí, sense cap dubte! Se’n diu portabilitat. Però si vols, també es pot canviar.

    +
    +
  • +
  • + Contractar dades addicionals +
    +

    Si esgotes dades abans d’acabar el mes, tens l’opció de contractar dades addicionals que podràs utilitzar fins a acabar el mes:

    +
      +
    • 500 MB (2 €)*
    • +
    • 1 GB (3 €)*
    • +
    • 3 GB (5 €)*
    • +
    • 5 GB (6 €)*
    • +
    • 10 GB (9€)*
    • +
    +

    * IVA inclòs

    +

    Aquí t’expliquem com afegir dades extra.

    +

    Contractar dades addicionals en les tarifes compartides

    +

    Si els mòbils que estan compartint dades esgoten els 50 GB abans que acabi el més, es pot comprar una ampliació de 10 GB per 9€ o de 20 GB per 12€ que podran consumir els diferents mòbils. Aquí t’expliquem com afegir dades extra a les tarifes compartides.

    +
    +
  • +
  • + Preus SMS +
    +

    Els SMS tenen un preu de 0,10€/sms

    +

    IMPORTANT: els SMS no estan inclosos a cap de les tarifes de Som Connexió. Tampoc a la tarifa de minuts il·limitats.

    +
    +
  • +
  • + Preus de trucades fora de tarifa des del mòbil +
    +

    Aquests són els preus de les trucades un cop has superat els minuts que tens contractats a la teva tarifa:

    +

    0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa.

    +

    A partir del minut 6, es paga 0,036€/min.

    +

    Consulta les tarifes de roaming AQUÍ.

    +
    +
  • +
  • + Tarifa bàsica mòbil: 0 min i 0 Gb +
    +

    Amb un cost base de 2€ mensuals, es tracta d’un consum “a granel”, és a dir, que es paga per minut i per unitat de dades consumides.

    +

    Aquesta tarifa és adequada per a persones que utilitzen el mòbil per estar localitzables i fer una trucada molt puntualment (només surt a compte si es fan menys de 6 trucades al mes de curta durada) i no utilitza el mòbil per connectar-se a Internet o ho fa de una manera molt esporàdica.

    +

    El preu per trucada en aquest abonament és de 0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa. A partir del minut 6, es paga 0,036€/min

    +

    El preu del servei de dades quan no s’ha contractat cap abonament és de 0,036€/Mb

    +

    Si vols desactivar el servei de dades, ens ho has de sol·licitar a serveis@somconnexio.coop.

    +
    +
  • +
  • + NÚMEROS DE TARIFACIÓ ESPECIAL +
    +

    Els preus estàndard de veu o missatges exclouen:

    +
      +
    • Els serveis de tarifació especial (per exemple: 80x, 90x o SMS Premium). Mira la taula de tarifes AQUÍ
    • +
    • Trànsit internacional
    • +
    • Trànsit en itinerància
    • +
    +

    Alguns dels serveis de tarifació especial de cost elevat estan bloquejats per defecte. Per tenir més informació sobre l’opció de bloqueig dels números de cost especial, consulta aquesta TAULA.

    +

    En aquest enllaç de la OCU ens donen més informació sobre els números de tarificació especial.

    +
    +
  • +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/urls.py b/promotion/urls.py index 8fd5eb7..2925480 100644 --- a/promotion/urls.py +++ b/promotion/urls.py @@ -11,4 +11,8 @@ urlpatterns = [ name="show_promotion"), path('select_wallet', views.SelectWalletView.as_view(), name="select_wallet"), + path('contract', views.ContractView.as_view(), + name="contract"), + path('contract/1', views.ThanksView.as_view(), + name="thanks"), ] diff --git a/promotion/views.py b/promotion/views.py index dcde3f0..0ed4455 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -6,11 +6,21 @@ from django.template.loader import get_template from django.urls import reverse_lazy from django.http import HttpResponse +from oidc4vp.models import Authorization from promotion.forms import WalletForm, ContractForm class PromotionView(View): - template_name = "somconnexio.tarifes-mobil.html" + template_name = "somconnexio_tarifes-mobil.html" + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + +class ThanksView(View): + template_name = "somconnexio_thanks.html" def get(self, request, *args, **kwargs): self.context = {} template = get_template( @@ -19,26 +29,38 @@ class PromotionView(View): return HttpResponse(template) -class PromotionMobile1View(FormView): +class ContractView(FormView): template_name = "somconnexio_contract.html" promotion = None - vp_tokens = None + vp_token = None authorization = None form_class = ContractForm - def get(self, request, *args, **kwargs): + success_url = reverse_lazy('promotion:thanks') + + def get_context_data(self, **kwargs): + # import pdb; pdb.set_trace() + self.context = super().get_context_data(**kwargs) code = self.request.GET.get("code") self.get_discount(code) - self.context = { + self.context.update({ "promotion": self.promotion, - "verificable_presentation": self.vp_token - } - template = get_template( - self.template_name, - ).render() - return HttpResponse(template) - + "verificable_presentation": self.vp_token, + "sim": 10.0, + "mensual": 15.0, + "total": 25.0 + }) + if self.promotion: + self.context['sim'] = self.context.get_discount(self.context["sim"]) + self.context['mensual'] = self.context.get_discount(self.context["mensual"]) + self.context['total'] = self.context.get_discount(self.context["total"]) + return self.context + + def get_form_kwargs(self): kwargs = super().get_form_kwargs() + if not self.vp_token: + return kwargs + self.vp_token.get_user_info() kwargs['verificable_presentation'] = self.vp_token kwargs["nif"] = self.vp_token.user_info.get("nif", '') @@ -58,6 +80,9 @@ class PromotionMobile1View(FormView): return redirect(url) def get_discount(self, code): + if not code: + return + self.authorization = Authorization.objects.filter( code=code, code_unused=False From f81394e1f339a8204ae6220ea0a5b16492cc1e67 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 12:19:57 +0100 Subject: [PATCH 39/63] fix link to normal contract --- promotion/forms.py | 1 + promotion/templates/somconnexio_tarifes_mobil.html | 1 + promotion/views.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/promotion/forms.py b/promotion/forms.py index d7af389..9ef48dc 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -61,3 +61,4 @@ class ContractForm(forms.Form): birthday = forms.CharField() gen = forms.CharField() lang = forms.CharField() + diff --git a/promotion/templates/somconnexio_tarifes_mobil.html b/promotion/templates/somconnexio_tarifes_mobil.html index e65b856..3016e59 100644 --- a/promotion/templates/somconnexio_tarifes_mobil.html +++ b/promotion/templates/somconnexio_tarifes_mobil.html @@ -529,6 +529,7 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+
El més preguntat
diff --git a/promotion/views.py b/promotion/views.py index 0ed4455..fbc6343 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -11,7 +11,7 @@ from promotion.forms import WalletForm, ContractForm class PromotionView(View): - template_name = "somconnexio_tarifes-mobil.html" + template_name = "somconnexio_tarifes_mobil.html" def get(self, request, *args, **kwargs): self.context = {} template = get_template( @@ -76,8 +76,7 @@ class ContractView(FormView): return kwargs def form_valid(self, form): - url = form.save() - return redirect(url) + return super().form_valid(form) def get_discount(self, code): if not code: From c65a8639c1bd108494fad7b3a5cf993a7a857331 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 13:41:19 +0100 Subject: [PATCH 40/63] fix form --- promotion/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/forms.py b/promotion/forms.py index 9ef48dc..5b96423 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization, Authorization +from promotion.models import Promotion class WalletForm(forms.Form): From 17faf5fff4741eda5d995d891263d2afb8f6ff97 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 13:55:56 +0100 Subject: [PATCH 41/63] sync command --- idhub/management/commands/initial_datas.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 2ce6640..fa9feb3 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from decouple import config from oidc4vp.models import Organization +from promotion.models import Promotion User = get_user_model() @@ -29,6 +30,7 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) + self.sync_credentials_organizations() def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) @@ -42,3 +44,11 @@ class Command(BaseCommand): def create_organizations(self, name, url): Organization.objects.create(name=name, response_uri=url) + + def sync_credentials_organizations(self): + org1 = Organization.objects.get(name="test1") + org2 = Organization.objects.get(name="test2") + org2.my_client_id = org1.client_id + org2.my_client_secret = org1.client_secret + org1.my_client_id = org2.client_id + org1.my_client_secret = org2.client_secret From 1be7a1829337e055a0e9600408926990e964961b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 14:06:43 +0100 Subject: [PATCH 42/63] fix bollean --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index b29349c..ec3667d 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -106,7 +106,7 @@ class Authorization(models.Model): The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ code = models.CharField(max_length=24, default=set_code) - code_used = models.BooleanField() + code_used = models.BooleanField(default=False) created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( From 6cf77c55db79eabbaa52554684e363cbabe2a659 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 14:07:59 +0100 Subject: [PATCH 43/63] . --- promotion/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/promotion/models.py b/promotion/models.py index 2750b79..76425b8 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -26,4 +26,5 @@ class Promotion(models.Model): ) def get_discount(self, price): - return price - price*0.25 \ No newline at end of file + return price - price*0.25 + From de3819b2347912c94a2f8691e4207264412ba931 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:38:16 +0100 Subject: [PATCH 44/63] fixing --- idhub/management/commands/initial_datas.py | 2 ++ oidc4vp/views.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index fa9feb3..cc45eec 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -52,3 +52,5 @@ class Command(BaseCommand): org2.my_client_secret = org1.client_secret org1.my_client_id = org2.client_id org1.my_client_secret = org2.client_secret + org1.save() + org2.save() diff --git a/oidc4vp/views.py b/oidc4vp/views.py index dbdac7d..ea592d4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.contrib import messages -from oidc4vp.models import Authorization, Organization +from oidc4vp.models import Authorization, Organization, OAuth2VPToken from idhub.mixins import UserView from oidc4vp.forms import AuthorizeForm @@ -34,13 +34,13 @@ class AuthorizeView(UserView, FormView): vps = json.loads(self.request.GET.get('presentation_definition')) except: vps = [] - # import pdb; pdb.set_trace() kwargs['presentation_definition'] = vps kwargs["org"] = self.get_org() kwargs["code"] = self.request.GET.get('code') return kwargs def form_valid(self, form): + # import pdb; pdb.set_trace() authorization = form.save() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) @@ -67,6 +67,7 @@ class AuthorizeView(UserView, FormView): return super().form_valid(form) def get_org(self): + # import pdb; pdb.set_trace() client_id = self.request.GET.get("client_id") if not client_id: raise Http404("Organization not found!") From c84bea8d06a5ee4d0b6d13591c6858f265435911 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:39:50 +0100 Subject: [PATCH 45/63] fixing --- oidc4vp/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ec3667d..ca449c7 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -1,3 +1,4 @@ +import json import requests import secrets From 0a8690659fff3fd0ec335801b1f57c9ccccd286b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:49:39 +0100 Subject: [PATCH 46/63] fix --- promotion/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/models.py b/promotion/models.py index 76425b8..4401faf 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -24,6 +24,7 @@ class Promotion(models.Model): reverse_lazy("promotion:contract"), code ) + return url def get_discount(self, price): return price - price*0.25 From 7b46501289d5fa9db2203ada918424b30ab887cf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:50:33 +0100 Subject: [PATCH 47/63] fix --- oidc4vp/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index ea592d4..fd164bf 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -145,9 +145,9 @@ class AllowCodeView(View): code=code, code_used=False ) - if not self.authorization.promotions: + if not self.authorization.promotions.exists(): raise Http404("Page not Found!") - promotion = self.authorization.promotions[0] + promotion = self.authorization.promotions.all()[0] return redirect(promotion.get_url(code)) From 69f1b003bf38c5825180a9bba48d32ab5de7e4fc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:52:54 +0100 Subject: [PATCH 48/63] add endpoint allow_code --- oidc4vp/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index b151a85..d7b79be 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -11,4 +11,6 @@ urlpatterns = [ name="verify"), path('authorize', views.AuthorizeView.as_view(), name="authorize"), + path('allow_code', views.AllowCodeView.as_view(), + name="allow_code"), ] From be49195833ca50f66cfb15d8abdaa720cc5b1e6c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 16:26:20 +0100 Subject: [PATCH 49/63] . --- examples/organizations.csv | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index 45029bc..a2f02c7 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,6 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/oidc4vp/" -"test1";"http://localhost:8000/oidc4vp/" +"test1";"https://idhub1.demo.pangea.org:9000/oidc4vp/" +"test2";"https://idhub2.demo.pangea.org:9000/oidc4vp/" +"test3";"http://localhost:9000/oidc4vp/" +"test4";"http://localhost:8000/oidc4vp/" From 6e6b8c46461fe9acb5c3368f5fdcccfe4aba98b2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 16:27:43 +0100 Subject: [PATCH 50/63] . --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index a2f02c7..af74c61 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,6 +1,6 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test1";"https://idhub1.demo.pangea.org:9000/oidc4vp/" -"test2";"https://idhub2.demo.pangea.org:9000/oidc4vp/" +"test1";"https://idhub1.demo.pangea.org/oidc4vp/" +"test2";"https://idhub2.demo.pangea.org/oidc4vp/" "test3";"http://localhost:9000/oidc4vp/" "test4";"http://localhost:8000/oidc4vp/" From f296cd020ce463ca86d897a6ec0d5ec6ab9269c0 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 18:40:37 +0100 Subject: [PATCH 51/63] fix render user info --- oidc4vp/models.py | 7 ++----- oidc4vp/views.py | 5 ++--- promotion/views.py | 46 ++++++++++++++++++++++++++-------------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ca449c7..66ceff1 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -168,7 +168,7 @@ class OAuth2VPToken(models.Model): authorization = models.ForeignKey( Authorization, on_delete=models.SET_NULL, - related_name='oauth2vptoken', + related_name='vp_tokens', null=True, ) @@ -176,10 +176,7 @@ class OAuth2VPToken(models.Model): code = kwargs.pop("code", None) super().__init__(*args, **kwargs) - self.authorization = get_object_or_404( - Authorization, - code=code - ) + self.authorization = Authorization.objects.filter(code=code).first() def verifing(self): self.result_verify = verify_presentation(self.vp_token) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index fd164bf..3f34bfd 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -40,7 +40,6 @@ class AuthorizeView(UserView, FormView): return kwargs def form_valid(self, form): - # import pdb; pdb.set_trace() authorization = form.save() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) @@ -67,7 +66,6 @@ class AuthorizeView(UserView, FormView): return super().form_valid(form) def get_org(self): - # import pdb; pdb.set_trace() client_id = self.request.GET.get("client_id") if not client_id: raise Http404("Organization not found!") @@ -93,7 +91,6 @@ class VerifyView(View): return HttpResponse(res) def post(self, request, *args, **kwargs): - # import pdb; pdb.set_trace() code = self.request.POST.get("code") vp_tk = self.request.POST.get("vp_token") @@ -107,6 +104,8 @@ class VerifyView(View): organization=org, code=code ) + if not vp_token.authorization: + raise Http404("Page not Found!") vp_token.verifing() response = vp_token.get_response_verify() diff --git a/promotion/views.py b/promotion/views.py index fbc6343..79b47ea 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -38,7 +38,6 @@ class ContractView(FormView): success_url = reverse_lazy('promotion:thanks') def get_context_data(self, **kwargs): - # import pdb; pdb.set_trace() self.context = super().get_context_data(**kwargs) code = self.request.GET.get("code") self.get_discount(code) @@ -50,29 +49,34 @@ class ContractView(FormView): "total": 25.0 }) if self.promotion: - self.context['sim'] = self.context.get_discount(self.context["sim"]) - self.context['mensual'] = self.context.get_discount(self.context["mensual"]) - self.context['total'] = self.context.get_discount(self.context["total"]) + self.context['sim'] = self.promotion.get_discount(self.context["sim"]) + self.context['mensual'] = self.promotion.get_discount(self.context["mensual"]) + self.context['total'] = self.promotion.get_discount(self.context["total"]) + + if self.vp_token: + self.context['verificable_presentation'] = self.vp_token + return self.context def get_form_kwargs(self): kwargs = super().get_form_kwargs() + code = self.request.GET.get("code") + self.get_discount(code) if not self.vp_token: return kwargs self.vp_token.get_user_info() - kwargs['verificable_presentation'] = self.vp_token - kwargs["nif"] = self.vp_token.user_info.get("nif", '') - kwargs["name"] = self.vp_token.user_info.get("name", '') - kwargs["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') - kwargs["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') - kwargs["email"] = self.vp_token.user_info.get("email", '') - kwargs["email_repeat"] = self.vp_token.user_info.get("email", '') - kwargs["telephone"] = self.vp_token.user_info.get("telephone", '') - kwargs["birthday"] = self.vp_token.user_info.get("birthday", '') - kwargs["gen"] = self.vp_token.user_info.get("gen", '') - kwargs["lang"] = self.vp_token.user_info.get("lang", '') + kwargs['initial']["nif"] = self.vp_token.user_info.get("nif", '') + kwargs['initial']["name"] = self.vp_token.user_info.get("name", '') + kwargs['initial']["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') + kwargs['initial']["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') + kwargs['initial']["email"] = self.vp_token.user_info.get("email", '') + kwargs['initial']["email_repeat"] = self.vp_token.user_info.get("email", '') + kwargs['initial']["telephone"] = self.vp_token.user_info.get("telephone", '') + kwargs['initial']["birthday"] = self.vp_token.user_info.get("birthday", '') + kwargs['initial']["gen"] = self.vp_token.user_info.get("gen", '') + kwargs['initial']["lang"] = self.vp_token.user_info.get("lang", '') return kwargs def form_valid(self, form): @@ -81,16 +85,18 @@ class ContractView(FormView): def get_discount(self, code): if not code: return + if self.authorization: + return self.authorization = Authorization.objects.filter( code=code, - code_unused=False + code_used=False ).first() if self.authorization: - if self.authorization.promotions: - self.promotion = self.authorization.promotionsp[-1] - if self.authorization.vp_tokens: - self.vp_tokens = self.authorization.vp_tokens[-1] + if self.authorization.promotions.exists(): + self.promotion = self.authorization.promotions.all()[0] + if self.authorization.vp_tokens.exists(): + self.vp_token = self.authorization.vp_tokens.all()[0] class SelectWalletView(FormView): From 267336658233c7546b31c31a8c61b1b933fe60af Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:10:14 +0100 Subject: [PATCH 52/63] . --- promotion/templates/somconnexio_contract.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 71d0cb2..70627f7 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -526,8 +526,6 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
-

HOLA HOLA

-
From cfe577ae726e4b2f4a3fd6b95b67d57276384a58 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:11:59 +0100 Subject: [PATCH 53/63] . --- promotion/templates/somconnexio_contract.html | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 70627f7..7b7779e 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -527,6 +527,7 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+

Tarifes Mòbil - Contractar

{% load i18n %} From efcc794c3e7b1adc9adeb1a8f2dcd7d1924f8de3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:16:06 +0100 Subject: [PATCH 54/63] fix return code as response --- oidc4vp/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 3f34bfd..3e6f0f3 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -110,8 +110,9 @@ class VerifyView(View): vp_token.verifing() response = vp_token.get_response_verify() vp_token.save() - if response["redirect_uri"]: - response["response"] = "Validation Code 255255255" + if not vp_token.authorization.promotions.exists(): + response["redirect_uri"] = "" + response["response"] = "Validation Code {}".format(code) return JsonResponse(response) From 5bb02bc37524166d8e9cdfeb0943c57831bfbb43 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 20:06:53 +0100 Subject: [PATCH 55/63] integrate more organizations --- idhub/management/commands/initial_datas.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index cc45eec..982bb97 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -30,7 +30,8 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) - self.sync_credentials_organizations() + self.sync_credentials_organizations("test1", "test2") + self.sync_credentials_organizations("test3", "test4") def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) @@ -45,9 +46,9 @@ class Command(BaseCommand): def create_organizations(self, name, url): Organization.objects.create(name=name, response_uri=url) - def sync_credentials_organizations(self): - org1 = Organization.objects.get(name="test1") - org2 = Organization.objects.get(name="test2") + def sync_credentials_organizations(self, test1, test2): + org1 = Organization.objects.get(name=test1) + org2 = Organization.objects.get(name=test2) org2.my_client_id = org1.client_id org2.my_client_secret = org1.client_secret org1.my_client_id = org2.client_id From f7a5ce52ee3dcc84b8623426ca949cf938143aaf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:22:43 +0100 Subject: [PATCH 56/63] postgres as a option --- requirements.txt | 1 + trustchain_idhub/settings.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f009565..9b19238 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 +dj-database-url==2.1.0 diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 274416d..7a3e043 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ import os from ast import literal_eval +from dj_database_url import parse as db_url from pathlib import Path from django.contrib.messages import constants as messages @@ -112,10 +113,15 @@ WSGI_APPLICATION = 'trustchain_idhub.wsgi.application' # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', + # } + 'default': config( + 'DATABASE_URL', + default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'), + cast=db_url + ) # 'default': config( # 'DATABASE_URL', # default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'), From db058178d1a34a9573abc9b81523d1300ead16d2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:23:10 +0100 Subject: [PATCH 57/63] change names of organizations --- examples/organizations.csv | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index af74c61..2f447ef 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,6 +1,5 @@ -"ExO";"https://verify.exo.cat" -"Somos Connexión";"https://verify.somosconexion.coop" -"test1";"https://idhub1.demo.pangea.org/oidc4vp/" -"test2";"https://idhub2.demo.pangea.org/oidc4vp/" -"test3";"http://localhost:9000/oidc4vp/" -"test4";"http://localhost:8000/oidc4vp/" +"pangea.org";"https://idhub1.demo.pangea.org/oidc4vp/" +"somconnexio.coop";"https://idhub2.demo.pangea.org/oidc4vp/" +"exo.cat";"https://verify.exo.cat" +"local 9000";"http://localhost:9000/oidc4vp/" +"local 8000";"http://localhost:8000/oidc4vp/" From 79dcb42c237c8ebf9415ae84a35c4c4f90d846b5 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:26:17 +0100 Subject: [PATCH 58/63] fix command initial_datas --- idhub/management/commands/initial_datas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 982bb97..1d44558 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -30,8 +30,8 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) - self.sync_credentials_organizations("test1", "test2") - self.sync_credentials_organizations("test3", "test4") + self.sync_credentials_organizations("pangea.org", "somconnexio.coop") + self.sync_credentials_organizations("local 8000", "local 9000") def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) From 40d662f62e0e50652ca036e9f8af1b81ed33faac Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:04 +0100 Subject: [PATCH 59/63] fix dids and credentials from the user --- idhub/admin/views.py | 2 +- idhub/user/views.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 99b6885..b6dcbc8 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -629,7 +629,7 @@ class DidsView(Credentials): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'dids': DID.objects, + 'dids': DID.objects.filter(user=self.request.user), }) return context diff --git a/idhub/user/views.py b/idhub/user/views.py index 0a3bfde..77ca3ec 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -81,8 +81,11 @@ class CredentialsView(MyWallet, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + creds = VerificableCredential.objects.filter( + user=self.request.user + ) context.update({ - 'credentials': VerificableCredential.objects, + 'credentials': creds, }) return context From b16cc85cd3dec15df3a01a452132c492e9ee8506 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:21 +0100 Subject: [PATCH 60/63] add more initial datas --- idhub/management/commands/initial_datas.py | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 1d44558..034bc68 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -1,10 +1,14 @@ import os import csv +import json from pathlib import Path +from utils import credtools +from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from decouple import config +from idhub.models import DID, Schemas from oidc4vp.models import Organization from promotion.models import Promotion @@ -32,6 +36,8 @@ class Command(BaseCommand): self.create_organizations(r[0].strip(), r[1].strip()) self.sync_credentials_organizations("pangea.org", "somconnexio.coop") self.sync_credentials_organizations("local 8000", "local 9000") + self.create_defaults_dids() + self.create_schemas() def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) @@ -55,3 +61,37 @@ class Command(BaseCommand): org1.my_client_secret = org2.client_secret org1.save() org2.save() + + def create_defaults_dids(self): + for u in User.objects.all(): + did = DID(label="Default", user=u) + did.set_did() + did.save() + + def create_schemas(self): + schemas_files = os.listdir(settings.SCHEMAS_DIR) + schemas = [x for x in schemas_files + if not Schemas.objects.filter(file_schema=x).exists()] + for x in schemas_files: + if Schemas.objects.filter(file_schema=x).exists(): + continue + self._create_schemas(x) + + def _create_schemas(self, file_name): + data = self.open_file(file_name) + try: + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name + except Exception: + return + Schemas.objects.create(file_schema=file_name, data=data, type=name) + + def open_file(self, file_name): + data = '' + filename = Path(settings.SCHEMAS_DIR).joinpath(file_name) + with filename.open() as schema_file: + data = schema_file.read() + + return data From 8abb43f95d4823ae4b07dd7a75a29ffc7ddaffdc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:48 +0100 Subject: [PATCH 61/63] remove schema --- schemas/member.json | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 schemas/member.json diff --git a/schemas/member.json b/schemas/member.json deleted file mode 100644 index 38199be..0000000 --- a/schemas/member.json +++ /dev/null @@ -1,21 +0,0 @@ - { - "$id": "https://pangea.org/schemas/member-credential-schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "name": "MemberCredential", - "description": "MemberCredential using JsonSchemaCredential", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "membershipType": { - "type": "string", - "enum": ["individual", "organization"] - } - }, - "required": ["name", "email", "membershipType"] - } From cb1b7de4f63260338ca8a8252670837c78844382 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 13 Dec 2023 17:52:18 +0100 Subject: [PATCH 62/63] simplify flow from promotion web --- idhub/user/views.py | 3 +++ oidc4vp/forms.py | 4 +++- oidc4vp/views.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index 77ca3ec..d7b1cb3 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -144,6 +144,9 @@ class CredentialsRequestView(MyWallet, FormView): messages.success(self.request, _("The credential was issued successfully!")) Event.set_EV_CREDENTIAL_ISSUED_FOR_USER(cred) Event.set_EV_CREDENTIAL_ISSUED(cred) + url = self.request.session.pop('next_url', None) + if url: + return redirect(url) else: messages.error(self.request, _("The credential does not exist!")) return super().form_valid(form) diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 5f6f6df..b3c4c9e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -23,8 +23,10 @@ class AuthorizeForm(forms.Form): reg = r'({})'.format('|'.join(self.presentation_definition)) - self.credentials = self.user.vcredentials.filter( + self.all_credentials = self.user.vcredentials.filter( schema__type__iregex=reg, + ) + self.credentials = self.all_credentials.filter( status=VerificableCredential.Status.ISSUED.value ) super().__init__(*args, **kwargs) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 3e6f0f3..a62e462 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -27,6 +27,12 @@ class AuthorizeView(UserView, FormView): form_class = AuthorizeForm success_url = reverse_lazy('idhub:user_demand_authorization') + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + if self.request.session.get('next_url'): + return redirect(reverse_lazy('idhub:user_credentials_request')) + return response + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user @@ -38,6 +44,12 @@ class AuthorizeView(UserView, FormView): kwargs["org"] = self.get_org() kwargs["code"] = self.request.GET.get('code') return kwargs + + def get_form(self, form_class=None): + form = super().get_form(form_class=form_class) + if form.all_credentials.exists() and not form.credentials.exists(): + self.request.session['next_url'] = self.request.get_full_path() + return form def form_valid(self, form): authorization = form.save() From d0dd59e098dabac89fc654724cf0634a88646178 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 13 Dec 2023 17:53:11 +0100 Subject: [PATCH 63/63] catalan as default --- trustchain_idhub/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 7a3e043..a798491 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -186,8 +186,9 @@ MESSAGE_TAGS = { LOCALE_PATHS = [ os.path.join(BASE_DIR, 'locale'), ] -LANGUAGE_CODE="en" +# LANGUAGE_CODE="en" # LANGUAGE_CODE="es" +LANGUAGE_CODE="ca" USE_I18N = True USE_L10N = True