resolve conflicts
This commit is contained in:
commit
87776751a4
|
@ -1,2 +1,5 @@
|
||||||
"ExO";"https://verify.exo.cat"
|
"pangea.org";"https://idhub1.demo.pangea.org/oidc4vp/"
|
||||||
"Somos Connexión";"https://verify.somosconexion.coop"
|
"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/"
|
||||||
|
|
|
|
@ -19,9 +19,9 @@ from idhub_auth.models import User
|
||||||
|
|
||||||
|
|
||||||
class ImportForm(forms.Form):
|
class ImportForm(forms.Form):
|
||||||
did = forms.ChoiceField(choices=[])
|
did = forms.ChoiceField(label=_("Did"), choices=[])
|
||||||
schema = forms.ChoiceField(choices=[])
|
schema = forms.ChoiceField(label=_("Schema"), choices=[])
|
||||||
file_import = forms.FileField()
|
file_import = forms.FileField(label=_("File import"))
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._schema = None
|
self._schema = None
|
||||||
|
@ -139,7 +139,7 @@ class ImportForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class SchemaForm(forms.Form):
|
class SchemaForm(forms.Form):
|
||||||
file_template = forms.FileField()
|
file_template = forms.FileField(label=_("File template"))
|
||||||
|
|
||||||
|
|
||||||
class MembershipForm(forms.ModelForm):
|
class MembershipForm(forms.ModelForm):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from idhub.models import Rol, Event
|
from idhub.models import Rol, Event
|
||||||
from idhub_auth.models import User
|
from idhub_auth.models import User
|
||||||
|
|
||||||
|
@ -18,10 +19,6 @@ class RolesTable(tables.Table):
|
||||||
|
|
||||||
|
|
||||||
class DashboardTable(tables.Table):
|
class DashboardTable(tables.Table):
|
||||||
type = tables.Column(verbose_name="Event")
|
|
||||||
message = tables.Column(verbose_name="Description")
|
|
||||||
created = tables.Column(verbose_name="Date")
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
template_name = "idhub/custom_table.html"
|
template_name = "idhub/custom_table.html"
|
||||||
|
|
|
@ -629,7 +629,7 @@ class DidsView(Credentials):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context.update({
|
context.update({
|
||||||
'dids': DID.objects,
|
'dids': DID.objects.filter(user=self.request.user),
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -772,11 +772,14 @@ class SchemasNewView(SchemasMix):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
data = f.read().decode('utf-8')
|
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:
|
except Exception:
|
||||||
messages.error(self.request, _('This is not a valid schema!'))
|
messages.error(self.request, _('This is not a valid schema!'))
|
||||||
return
|
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()
|
schema.save()
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
@ -817,11 +820,14 @@ class SchemasImportAddView(SchemasMix):
|
||||||
def create_schema(self, file_name):
|
def create_schema(self, file_name):
|
||||||
data = self.open_file(file_name)
|
data = self.open_file(file_name)
|
||||||
try:
|
try:
|
||||||
json.loads(data)
|
ldata = json.loads(data)
|
||||||
|
assert credtools.validate_schema(ldata)
|
||||||
|
name = ldata.get('name')
|
||||||
|
assert name
|
||||||
except Exception:
|
except Exception:
|
||||||
messages.error(self.request, _('This is not a valid schema!'))
|
messages.error(self.request, _('This is not a valid schema!'))
|
||||||
return
|
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()
|
schema.save()
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
@ -7,6 +9,9 @@ from django.utils.encoding import force_bytes
|
||||||
from django.utils.http import urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_encode
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NotifyActivateUserByEmail:
|
class NotifyActivateUserByEmail:
|
||||||
def get_email_context(self, user):
|
def get_email_context(self, user):
|
||||||
"""
|
"""
|
||||||
|
@ -49,10 +54,11 @@ class NotifyActivateUserByEmail:
|
||||||
email_message.attach_alternative(html_email, 'text/html')
|
email_message.attach_alternative(html_email, 'text/html')
|
||||||
try:
|
try:
|
||||||
if settings.DEVELOPMENT:
|
if settings.DEVELOPMENT:
|
||||||
print(to_email)
|
logger.warning(to_email)
|
||||||
print(body)
|
logger.warning(body)
|
||||||
return
|
return
|
||||||
|
|
||||||
email_message.send()
|
email_message.send()
|
||||||
except Exception:
|
except Exception as err:
|
||||||
|
logger.error(err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
|
import json
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from utils import credtools
|
||||||
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from idhub.models import Organization
|
from idhub.models import DID, Schemas
|
||||||
|
from oidc4vp.models import Organization
|
||||||
|
from promotion.models import Promotion
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
@ -29,6 +34,10 @@ class Command(BaseCommand):
|
||||||
f = csv.reader(csvfile, delimiter=';', quotechar='"')
|
f = csv.reader(csvfile, delimiter=';', quotechar='"')
|
||||||
for r in f:
|
for r in f:
|
||||||
self.create_organizations(r[0].strip(), r[1].strip())
|
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):
|
def create_admin_users(self, email, password):
|
||||||
su = User.objects.create_superuser(email=email, password=password)
|
su = User.objects.create_superuser(email=email, password=password)
|
||||||
|
@ -44,4 +53,48 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
|
|
||||||
def create_organizations(self, name, url):
|
def create_organizations(self, name, url):
|
||||||
Organization.objects.create(name=name, url=url)
|
Organization.objects.create(name=name, response_uri=url)
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.5 on 2024-01-04 15:12
|
# Generated by Django 4.2.5 on 2024-01-04 16:59
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
('created_at', models.DateTimeField(auto_now=True)),
|
('created_at', models.DateTimeField(auto_now=True)),
|
||||||
('label', models.CharField(max_length=50)),
|
('label', models.CharField(max_length=50, verbose_name='Label')),
|
||||||
('did', models.CharField(max_length=250)),
|
('did', models.CharField(max_length=250)),
|
||||||
('_key_material', models.BinaryField(max_length=250)),
|
('_key_material', models.BinaryField(max_length=250)),
|
||||||
(
|
(
|
||||||
|
@ -57,27 +57,6 @@ class Migration(migrations.Migration):
|
||||||
('created_at', models.DateTimeField(auto_now=True)),
|
('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(
|
migrations.CreateModel(
|
||||||
name='Rol',
|
name='Rol',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -111,6 +90,7 @@ class Migration(migrations.Migration):
|
||||||
verbose_name='ID',
|
verbose_name='ID',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
('type', models.CharField(max_length=250)),
|
||||||
('file_schema', models.CharField(max_length=250)),
|
('file_schema', models.CharField(max_length=250)),
|
||||||
('data', models.TextField()),
|
('data', models.TextField()),
|
||||||
('created_at', models.DateTimeField(auto_now=True)),
|
('created_at', models.DateTimeField(auto_now=True)),
|
||||||
|
@ -168,8 +148,7 @@ class Migration(migrations.Migration):
|
||||||
('verified', models.BooleanField()),
|
('verified', models.BooleanField()),
|
||||||
('created_on', models.DateTimeField(auto_now=True)),
|
('created_on', models.DateTimeField(auto_now=True)),
|
||||||
('issued_on', models.DateTimeField(null=True)),
|
('issued_on', models.DateTimeField(null=True)),
|
||||||
('subject_did', models.CharField(max_length=250)),
|
('data', models.TextField()),
|
||||||
('_data', models.BinaryField()),
|
|
||||||
('csv_data', models.TextField()),
|
('csv_data', models.TextField()),
|
||||||
(
|
(
|
||||||
'status',
|
'status',
|
||||||
|
@ -199,6 +178,15 @@ class Migration(migrations.Migration):
|
||||||
to='idhub.schemas',
|
to='idhub.schemas',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'subject_did',
|
||||||
|
models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='subject_credentials',
|
||||||
|
to='idhub.did',
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'user',
|
'user',
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
|
@ -268,8 +256,11 @@ class Migration(migrations.Migration):
|
||||||
verbose_name='ID',
|
verbose_name='ID',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
('created', models.DateTimeField(auto_now=True)),
|
('created', models.DateTimeField(auto_now=True, verbose_name='Date')),
|
||||||
('message', models.CharField(max_length=350)),
|
(
|
||||||
|
'message',
|
||||||
|
models.CharField(max_length=350, verbose_name='Description'),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'type',
|
'type',
|
||||||
models.PositiveSmallIntegerField(
|
models.PositiveSmallIntegerField(
|
||||||
|
@ -307,7 +298,8 @@ class Migration(migrations.Migration):
|
||||||
(28, 'Organisational DID deleted by admin'),
|
(28, 'Organisational DID deleted by admin'),
|
||||||
(29, 'User deactivated'),
|
(29, 'User deactivated'),
|
||||||
(30, 'User activated'),
|
(30, 'User activated'),
|
||||||
]
|
],
|
||||||
|
verbose_name='Event',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -339,6 +331,7 @@ class Migration(migrations.Migration):
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name='users',
|
related_name='users',
|
||||||
to='idhub.service',
|
to='idhub.service',
|
||||||
|
verbose_name='Service',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import json
|
import json
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
|
||||||
import datetime
|
import datetime
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -50,9 +49,10 @@ class Event(models.Model):
|
||||||
EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated"
|
EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated"
|
||||||
EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated"
|
EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated"
|
||||||
|
|
||||||
created = models.DateTimeField(auto_now=True)
|
created = models.DateTimeField(_("Date"), auto_now=True)
|
||||||
message = models.CharField(max_length=350)
|
message = models.CharField(_("Description"), max_length=350)
|
||||||
type = models.PositiveSmallIntegerField(
|
type = models.PositiveSmallIntegerField(
|
||||||
|
_("Event"),
|
||||||
choices=Types.choices,
|
choices=Types.choices,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
|
@ -407,7 +407,7 @@ class Event(models.Model):
|
||||||
|
|
||||||
class DID(models.Model):
|
class DID(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now=True)
|
created_at = models.DateTimeField(auto_now=True)
|
||||||
label = models.CharField(max_length=50)
|
label = models.CharField(_("Label"), max_length=50)
|
||||||
did = models.CharField(max_length=250)
|
did = models.CharField(max_length=250)
|
||||||
# In JWK format. Must be stored as-is and passed whole to library functions.
|
# In JWK format. Must be stored as-is and passed whole to library functions.
|
||||||
# Example key material:
|
# Example key material:
|
||||||
|
@ -459,6 +459,7 @@ class DID(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Schemas(models.Model):
|
class Schemas(models.Model):
|
||||||
|
type = models.CharField(max_length=250)
|
||||||
file_schema = models.CharField(max_length=250)
|
file_schema = models.CharField(max_length=250)
|
||||||
data = models.TextField()
|
data = models.TextField()
|
||||||
created_at = models.DateTimeField(auto_now=True)
|
created_at = models.DateTimeField(auto_now=True)
|
||||||
|
@ -490,11 +491,7 @@ class VerificableCredential(models.Model):
|
||||||
verified = models.BooleanField()
|
verified = models.BooleanField()
|
||||||
created_on = models.DateTimeField(auto_now=True)
|
created_on = models.DateTimeField(auto_now=True)
|
||||||
issued_on = models.DateTimeField(null=True)
|
issued_on = models.DateTimeField(null=True)
|
||||||
subject_did = models.CharField(max_length=250)
|
data = models.TextField()
|
||||||
# CHANGED: `data` to `_data`, datatype from TextField to BinaryField and the rendered VC is now stored encrypted.
|
|
||||||
# TODO: verify that BinaryField can hold arbitrary amounts of data (max_length = ???)
|
|
||||||
data = None
|
|
||||||
_data = models.BinaryField()
|
|
||||||
csv_data = models.TextField()
|
csv_data = models.TextField()
|
||||||
status = models.PositiveSmallIntegerField(
|
status = models.PositiveSmallIntegerField(
|
||||||
choices=Status.choices,
|
choices=Status.choices,
|
||||||
|
@ -505,6 +502,12 @@ class VerificableCredential(models.Model):
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='vcredentials',
|
related_name='vcredentials',
|
||||||
)
|
)
|
||||||
|
subject_did = models.ForeignKey(
|
||||||
|
DID,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='subject_credentials',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
issuer_did = models.ForeignKey(
|
issuer_did = models.ForeignKey(
|
||||||
DID,
|
DID,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -517,36 +520,16 @@ class VerificableCredential(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
key_dids = cache.get("KEY_DIDS", {})
|
return self.user.decrypt_data(self.data)
|
||||||
if not key_dids.get(user.id):
|
|
||||||
raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.")
|
|
||||||
sb = secret.SecretBox(key_dids[user.id])
|
|
||||||
return sb.decrypt(self._data)
|
|
||||||
|
|
||||||
def set_data(self, value):
|
def set_data(self, value):
|
||||||
key_dids = cache.get("KEY_DIDS", {})
|
self.data = self.user.encrypt_data(value)
|
||||||
if not key_dids.get(user.id):
|
|
||||||
raise Exception("Ojo! Se intenta acceder a datos cifrados sin tener la clave.")
|
|
||||||
sb = secret.SecretBox(key_dids[user.id])
|
|
||||||
self._data = sb.encrypt(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def get_schema(self):
|
|
||||||
if not self.data:
|
|
||||||
return {}
|
|
||||||
return json.loads(self.data)
|
|
||||||
|
|
||||||
def type(self):
|
def type(self):
|
||||||
if self.data:
|
return self.schema.type
|
||||||
return self.get_schema.get('type')[-1]
|
|
||||||
|
|
||||||
return self.schema.name()
|
|
||||||
|
|
||||||
def description(self):
|
def description(self):
|
||||||
if not self.data:
|
for des in json.loads(self.render()).get('description', []):
|
||||||
return self.schema.description()
|
|
||||||
|
|
||||||
for des in self.get_schema.get('description', []):
|
|
||||||
if settings.LANGUAGE_CODE == des.get('lang'):
|
if settings.LANGUAGE_CODE == des.get('lang'):
|
||||||
return des.get('value', '')
|
return des.get('value', '')
|
||||||
return ''
|
return ''
|
||||||
|
@ -572,13 +555,15 @@ class VerificableCredential(models.Model):
|
||||||
|
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
d = json.loads(self.csv_data)
|
d = json.loads(self.csv_data)
|
||||||
|
issuance_date = ''
|
||||||
|
if self.issued_on:
|
||||||
format = "%Y-%m-%dT%H:%M:%SZ"
|
format = "%Y-%m-%dT%H:%M:%SZ"
|
||||||
issuance_date = self.issued_on.strftime(format)
|
issuance_date = self.issued_on.strftime(format)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'vc_id': self.id,
|
'vc_id': self.id,
|
||||||
'issuer_did': self.issuer_did.did,
|
'issuer_did': self.issuer_did.did,
|
||||||
'subject_did': self.subject_did,
|
'subject_did': self.subject_did and self.subject_did.did or '',
|
||||||
'issuance_date': issuance_date,
|
'issuance_date': issuance_date,
|
||||||
'first_name': self.user.first_name,
|
'first_name': self.user.first_name,
|
||||||
'last_name': self.user.last_name,
|
'last_name': self.user.last_name,
|
||||||
|
@ -677,24 +662,10 @@ class UserRol(models.Model):
|
||||||
)
|
)
|
||||||
service = models.ForeignKey(
|
service = models.ForeignKey(
|
||||||
Service,
|
Service,
|
||||||
|
verbose_name=_("Service"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='users',
|
related_name='users',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('user', 'service',)
|
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)
|
|
||||||
|
|
30
idhub/templates/credentials/exo.json
Normal file
30
idhub/templates/credentials/exo.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
idhub/templates/credentials/openarms.json
Normal file
33
idhub/templates/credentials/openarms.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
idhub/templates/credentials/paremanel.json
Normal file
30
idhub/templates/credentials/paremanel.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@
|
||||||
<td>{{ rol.name }}</td>
|
<td>{{ rol.name }}</td>
|
||||||
<td>{{ rol.description|default:""}}</td>
|
<td>{{ rol.description|default:""}}</td>
|
||||||
<td><a href="{% url 'idhub:admin_rol_edit' rol.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
<td><a href="{% url 'idhub:admin_rol_edit' rol.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
||||||
<td><a href="{% url 'idhub:admin_rol_del' rol.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
<td><a class="text-danger" href="{% url 'idhub:admin_rol_del' rol.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<td>{{ service.description }}</td>
|
<td>{{ service.description }}</td>
|
||||||
<td>{{ service.get_roles }}</td>
|
<td>{{ service.get_roles }}</td>
|
||||||
<td><a href="{% url 'idhub:admin_service_edit' service.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
<td><a href="{% url 'idhub:admin_service_edit' service.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
||||||
<td><a href="{% url 'idhub:admin_service_del' service.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
<td><a class="text-danger" href="{% url 'idhub:admin_service_del' service.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row border-bottom">
|
<div class="row border-bottom">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
First Name:
|
{% trans "First name" %}:
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 text-secondary">
|
<div class="col-9 text-secondary">
|
||||||
{{ object.first_name|default:'' }}
|
{{ object.first_name|default:'' }}
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row border-bottom mt-3">
|
<div class="row border-bottom mt-3">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
Last Name:
|
{% trans "Last name" %}:
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 text-secondary">
|
<div class="col-9 text-secondary">
|
||||||
{{ object.last_name|default:'' }}
|
{{ object.last_name|default:'' }}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
Email:
|
{% trans "Email address" %}:
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 text-secondary">
|
<div class="col-9 text-secondary">
|
||||||
{{ object.email }}
|
{{ object.email }}
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
<td>{{ membership.start_date|default:'' }}</td>
|
<td>{{ membership.start_date|default:'' }}</td>
|
||||||
<td>{{ membership.end_date|default:'' }}</td>
|
<td>{{ membership.end_date|default:'' }}</td>
|
||||||
<td><a href="{% url 'idhub:admin_people_membership_edit' membership.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
<td><a href="{% url 'idhub:admin_people_membership_edit' membership.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
||||||
<td><a href="{% url 'idhub:admin_people_membership_del' membership.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
<td><a class="text-danger" href="{% url 'idhub:admin_people_membership_del' membership.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
<td>{{ rol.service.description }}</td>
|
<td>{{ rol.service.description }}</td>
|
||||||
<td>{{ rol.service.domain }}</td>
|
<td>{{ rol.service.domain }}</td>
|
||||||
<td><a href="{% url 'idhub:admin_people_rol_edit' rol.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
<td><a href="{% url 'idhub:admin_people_rol_edit' rol.id %}" title="{% trans 'Edit' %}"><i class="bi bi-pencil-square"></i></a></td>
|
||||||
<td><a href="{% url 'idhub:admin_people_rol_del' rol.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
<td><a class="text-danger" href="{% url 'idhub:admin_people_rol_del' rol.id %}" title="{% trans 'Delete' %}"><i class="bi bi-trash"></i></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if path == 'user_credentials_presentation' %}active2{% endif %}" href="{% url 'idhub:user_credentials_presentation' %}">
|
<a class="nav-link {% if path in 'user_demand_authorization, authorize' %}active2{% endif %}" href="{% url 'idhub:user_demand_authorization' %}">
|
||||||
{% trans 'Present a credential' %}
|
{% trans 'Present a credential' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
<h1 class="h2">{{ title }}</h1>
|
<h1 class="h2">{{ title }}</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<input class="form-control form-control-grey " type="text" placeholder="Search" aria-label="Search">
|
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -171,7 +171,7 @@
|
||||||
<h1 class="h2">{{ title }}</h1>
|
<h1 class="h2">{{ title }}</h1>
|
||||||
<div class="btn-toolbar mb-2 mb-md-0">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<input class="form-control form-control-grey " type="text" placeholder="Search" aria-label="Search">
|
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy
|
||||||
from .views import LoginView, PasswordResetConfirmView
|
from .views import LoginView, PasswordResetConfirmView
|
||||||
from .admin import views as views_admin
|
from .admin import views as views_admin
|
||||||
from .user import views as views_user
|
from .user import views as views_user
|
||||||
|
# from .verification_portal import views as views_verification_portal
|
||||||
|
|
||||||
app_name = 'idhub'
|
app_name = 'idhub'
|
||||||
|
|
||||||
|
@ -87,9 +88,9 @@ urlpatterns = [
|
||||||
path('user/credentials/request/',
|
path('user/credentials/request/',
|
||||||
views_user.CredentialsRequestView.as_view(),
|
views_user.CredentialsRequestView.as_view(),
|
||||||
name='user_credentials_request'),
|
name='user_credentials_request'),
|
||||||
path('user/credentials_presentation/',
|
path('user/credentials_presentation/demand',
|
||||||
views_user.CredentialsPresentationView.as_view(),
|
views_user.DemandAuthorizationView.as_view(),
|
||||||
name='user_credentials_presentation'),
|
name='user_demand_authorization'),
|
||||||
|
|
||||||
# Admin
|
# Admin
|
||||||
path('admin/dashboard/', views_admin.DashboardView.as_view(),
|
path('admin/dashboard/', views_admin.DashboardView.as_view(),
|
||||||
|
@ -174,4 +175,7 @@ urlpatterns = [
|
||||||
name='admin_import'),
|
name='admin_import'),
|
||||||
path('admin/import/new', views_admin.ImportAddView.as_view(),
|
path('admin/import/new', views_admin.ImportAddView.as_view(),
|
||||||
name='admin_import_add'),
|
name='admin_import_add'),
|
||||||
|
|
||||||
|
# path('verification_portal/verify/', views_verification_portal.verify,
|
||||||
|
# name="verification_portal_verify")
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import requests
|
||||||
from django import forms
|
from django import forms
|
||||||
from idhub_auth.models import User
|
from django.conf import settings
|
||||||
from idhub.models import DID, VerificableCredential, Organization
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from idhub_auth.models import User
|
||||||
|
from idhub.models import DID, VerificableCredential
|
||||||
|
from oidc4vp.models import Organization
|
||||||
|
|
||||||
|
|
||||||
class ProfileForm(forms.ModelForm):
|
class ProfileForm(forms.ModelForm):
|
||||||
|
@ -13,8 +17,8 @@ class ProfileForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class RequestCredentialForm(forms.Form):
|
class RequestCredentialForm(forms.Form):
|
||||||
did = forms.ChoiceField(choices=[])
|
did = forms.ChoiceField(label=_("Did"), choices=[])
|
||||||
credential = forms.ChoiceField(choices=[])
|
credential = forms.ChoiceField(label=_("Credential"), choices=[])
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user', None)
|
self.user = kwargs.pop('user', None)
|
||||||
|
@ -42,7 +46,7 @@ class RequestCredentialForm(forms.Form):
|
||||||
if not all([cred.exists(), did.exists()]):
|
if not all([cred.exists(), did.exists()]):
|
||||||
return
|
return
|
||||||
|
|
||||||
did = did[0].did
|
did = did[0]
|
||||||
cred = cred[0]
|
cred = cred[0]
|
||||||
try:
|
try:
|
||||||
cred.issue(did)
|
cred.issue(did)
|
||||||
|
@ -56,42 +60,30 @@ class RequestCredentialForm(forms.Form):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class DemandAuthorizationForm(forms.Form):
|
||||||
class CredentialPresentationForm(forms.Form):
|
organization = forms.ChoiceField(label=_("Organization"), choices=[])
|
||||||
organization = forms.ChoiceField(choices=[])
|
|
||||||
credential = forms.ChoiceField(choices=[])
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop('user', None)
|
self.user = kwargs.pop('user', None)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.fields['organization'].choices = [
|
self.fields['organization'].choices = [
|
||||||
(x.id, x.name) for x in Organization.objects.filter()
|
(x.id, x.name) for x in Organization.objects.filter()
|
||||||
]
|
if x.response_uri != settings.RESPONSE_URI
|
||||||
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):
|
def save(self, commit=True):
|
||||||
self.org = Organization.objects.filter(
|
self.org = Organization.objects.filter(
|
||||||
id=self.data['organization']
|
id=self.data['organization']
|
||||||
)
|
)
|
||||||
self.cred = VerificableCredential.objects.filter(
|
if not self.org.exists():
|
||||||
user=self.user,
|
|
||||||
id=self.data['credential'],
|
|
||||||
status=VerificableCredential.Status.ISSUED
|
|
||||||
)
|
|
||||||
if not all([self.org.exists(), self.cred.exists()]):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.org = self.org[0]
|
self.org = self.org[0]
|
||||||
self.cred = self.cred[0]
|
|
||||||
|
|
||||||
if commit:
|
if commit:
|
||||||
self.org.send(self.cred)
|
url = self.org.demand_authorization()
|
||||||
return self.cred
|
if url.status_code == 200:
|
||||||
|
return url.json().get('redirect_uri')
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,11 @@ from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from idhub.user.forms import ProfileForm, RequestCredentialForm, CredentialPresentationForm
|
from idhub.user.forms import (
|
||||||
|
ProfileForm,
|
||||||
|
RequestCredentialForm,
|
||||||
|
DemandAuthorizationForm
|
||||||
|
)
|
||||||
from idhub.mixins import UserView
|
from idhub.mixins import UserView
|
||||||
from idhub.models import DID, VerificableCredential, Event
|
from idhub.models import DID, VerificableCredential, Event
|
||||||
|
|
||||||
|
@ -76,8 +80,11 @@ class CredentialsView(MyWallet, TemplateView):
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
creds = VerificableCredential.objects.filter(
|
||||||
|
user=self.request.user
|
||||||
|
)
|
||||||
context.update({
|
context.update({
|
||||||
'credentials': VerificableCredential.objects,
|
'credentials': creds,
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -136,17 +143,20 @@ class CredentialsRequestView(MyWallet, FormView):
|
||||||
messages.success(self.request, _("The credential was issued successfully!"))
|
messages.success(self.request, _("The credential was issued successfully!"))
|
||||||
Event.set_EV_CREDENTIAL_ISSUED_FOR_USER(cred)
|
Event.set_EV_CREDENTIAL_ISSUED_FOR_USER(cred)
|
||||||
Event.set_EV_CREDENTIAL_ISSUED(cred)
|
Event.set_EV_CREDENTIAL_ISSUED(cred)
|
||||||
|
url = self.request.session.pop('next_url', None)
|
||||||
|
if url:
|
||||||
|
return redirect(url)
|
||||||
else:
|
else:
|
||||||
messages.error(self.request, _("The credential does not exist!"))
|
messages.error(self.request, _("The credential does not exist!"))
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class CredentialsPresentationView(MyWallet, FormView):
|
class DemandAuthorizationView(MyWallet, FormView):
|
||||||
template_name = "idhub/user/credentials_presentation.html"
|
template_name = "idhub/user/credentials_presentation.html"
|
||||||
subtitle = _('Credential presentation')
|
subtitle = _('Credential presentation')
|
||||||
icon = 'bi bi-patch-check-fill'
|
icon = 'bi bi-patch-check-fill'
|
||||||
form_class = CredentialPresentationForm
|
form_class = DemandAuthorizationForm
|
||||||
success_url = reverse_lazy('idhub:user_credentials')
|
success_url = reverse_lazy('idhub:user_demand_authorization')
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
kwargs = super().get_form_kwargs()
|
||||||
|
@ -154,11 +164,17 @@ class CredentialsPresentationView(MyWallet, FormView):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
cred = form.save()
|
try:
|
||||||
if cred:
|
authorization = form.save()
|
||||||
Event.set_EV_CREDENTIAL_PRESENTED_BY_USER(cred, form.org)
|
except Exception:
|
||||||
Event.set_EV_CREDENTIAL_PRESENTED(cred, form.org)
|
txt = _("Problems connecting with {url}").format(
|
||||||
messages.success(self.request, _("The credential was presented successfully!"))
|
url=form.org.response_uri
|
||||||
|
)
|
||||||
|
messages.error(self.request, txt)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
if authorization:
|
||||||
|
return redirect(authorization)
|
||||||
else:
|
else:
|
||||||
messages.error(self.request, _("Error sending credential!"))
|
messages.error(self.request, _("Error sending credential!"))
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
0
idhub/verification_portal/__init__.py
Normal file
0
idhub/verification_portal/__init__.py
Normal file
24
idhub/verification_portal/models.py
Normal file
24
idhub/verification_portal/models.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
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 <destination> with <subject> and <body>"
|
||||||
|
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)
|
49
idhub/verification_portal/views.py
Normal file
49
idhub/verification_portal/views.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ProfileForm(forms.ModelForm):
|
||||||
def clean_first_name(self):
|
def clean_first_name(self):
|
||||||
first_name = super().clean()['first_name']
|
first_name = super().clean()['first_name']
|
||||||
match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+'
|
match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+'
|
||||||
if not re.match(match, first_name):
|
if not re.fullmatch(match, first_name):
|
||||||
txt = _("The string must contain only characters and spaces")
|
txt = _("The string must contain only characters and spaces")
|
||||||
raise forms.ValidationError(txt)
|
raise forms.ValidationError(txt)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class ProfileForm(forms.ModelForm):
|
||||||
def clean_last_name(self):
|
def clean_last_name(self):
|
||||||
last_name = super().clean()['last_name']
|
last_name = super().clean()['last_name']
|
||||||
match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+'
|
match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+'
|
||||||
if not re.match(match, last_name):
|
if not re.fullmatch(match, last_name):
|
||||||
txt = _("The string must contain only characters and spaces")
|
txt = _("The string must contain only characters and spaces")
|
||||||
raise forms.ValidationError(txt)
|
raise forms.ValidationError(txt)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 4.2.5 on 2024-01-04 15:12
|
# Generated by Django 4.2.5 on 2024-01-04 16:59
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -31,13 +31,23 @@ class Migration(migrations.Migration):
|
||||||
(
|
(
|
||||||
'email',
|
'email',
|
||||||
models.EmailField(
|
models.EmailField(
|
||||||
max_length=255, unique=True, verbose_name='email address'
|
max_length=255, unique=True, verbose_name='Email address'
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
('is_active', models.BooleanField(default=True)),
|
('is_active', models.BooleanField(default=True)),
|
||||||
('is_admin', models.BooleanField(default=False)),
|
('is_admin', models.BooleanField(default=False)),
|
||||||
('first_name', models.CharField(blank=True, max_length=255, null=True)),
|
(
|
||||||
('last_name', models.CharField(blank=True, max_length=255, null=True)),
|
'first_name',
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name='First name'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'last_name',
|
||||||
|
models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name='Last name'
|
||||||
|
),
|
||||||
|
),
|
||||||
('encrypted_sensitive_data', models.CharField(max_length=255)),
|
('encrypted_sensitive_data', models.CharField(max_length=255)),
|
||||||
('salt', models.CharField(max_length=255)),
|
('salt', models.CharField(max_length=255)),
|
||||||
],
|
],
|
||||||
|
|
|
@ -4,6 +4,7 @@ import base64
|
||||||
from nacl import pwhash
|
from nacl import pwhash
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
|
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,14 +41,14 @@ class UserManager(BaseUserManager):
|
||||||
|
|
||||||
class User(AbstractBaseUser):
|
class User(AbstractBaseUser):
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
verbose_name="email address",
|
_('Email address'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
is_admin = models.BooleanField(default=False)
|
is_admin = models.BooleanField(default=False)
|
||||||
first_name = models.CharField(max_length=255, blank=True, null=True)
|
first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True)
|
||||||
last_name = models.CharField(max_length=255, blank=True, null=True)
|
last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True)
|
||||||
encrypted_sensitive_data = models.CharField(max_length=255)
|
encrypted_sensitive_data = models.CharField(max_length=255)
|
||||||
salt = models.CharField(max_length=255)
|
salt = models.CharField(max_length=255)
|
||||||
|
|
||||||
|
@ -144,4 +145,21 @@ class User(AbstractBaseUser):
|
||||||
key_crypted = self.encrypt_sensitive_data(password, key)
|
key_crypted = self.encrypt_sensitive_data(password, key)
|
||||||
self.encrypted_sensitive_data = key_crypted
|
self.encrypted_sensitive_data = key_crypted
|
||||||
|
|
||||||
|
def encrypt_data(self, data):
|
||||||
|
sb = self.get_secret_box()
|
||||||
|
value = base64.b64encode(data.encode('utf-8'))
|
||||||
|
return sb.encrypt(data)
|
||||||
|
|
||||||
|
def decrypt_data(self, data):
|
||||||
|
sb = self.get_secret_box()
|
||||||
|
value = base64.b64decode(data.encode('utf-8'))
|
||||||
|
return sb.decrypt(data)
|
||||||
|
|
||||||
|
def get_secret_box(self):
|
||||||
|
key_dids = cache.get("KEY_DIDS", {})
|
||||||
|
if not key_dids.get(self.id):
|
||||||
|
err = "An attempt is made to access encrypted "
|
||||||
|
err += "data without having the key."
|
||||||
|
raise Exception(_(err))
|
||||||
|
|
||||||
|
return secret.SecretBox(key_dids[self.id])
|
||||||
|
|
BIN
locale/ca/LC_MESSAGES/django.mo
Normal file
BIN
locale/ca/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load diff
BIN
locale/es/LC_MESSAGES/django.mo
Normal file
BIN
locale/es/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
0
oidc4vp/__init__.py
Normal file
0
oidc4vp/__init__.py
Normal file
3
oidc4vp/admin.py
Normal file
3
oidc4vp/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
oidc4vp/apps.py
Normal file
6
oidc4vp/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class Oidc4VpConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'oidc4vp'
|
81
oidc4vp/forms.py
Normal file
81
oidc4vp/forms.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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
|
||||||
|
from idhub.models import VerificableCredential
|
||||||
|
|
||||||
|
|
||||||
|
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.code = kwargs.pop('code', None)
|
||||||
|
self.presentation_definition = kwargs.pop('presentation_definition', [])
|
||||||
|
|
||||||
|
reg = r'({})'.format('|'.join(self.presentation_definition))
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not self.code:
|
||||||
|
txt = _("There isn't code in request")
|
||||||
|
raise ValidationError(txt)
|
||||||
|
|
||||||
|
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, self.code)
|
||||||
|
|
||||||
|
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)
|
137
oidc4vp/migrations/0001_initial.py
Normal file
137
oidc4vp/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
# Generated by Django 4.2.5 on 2023-12-11 08:35
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
('code_used', models.BooleanField()),
|
||||||
|
('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, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'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(
|
||||||
|
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)),
|
||||||
|
('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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
0
oidc4vp/migrations/__init__.py
Normal file
0
oidc4vp/migrations/__init__.py
Normal file
218
oidc4vp/models.py
Normal file
218
oidc4vp/models.py
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
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(
|
||||||
|
max_length=24,
|
||||||
|
default=set_client_id,
|
||||||
|
unique=True
|
||||||
|
)
|
||||||
|
client_secret = models.CharField(
|
||||||
|
max_length=48,
|
||||||
|
default=set_client_secret
|
||||||
|
)
|
||||||
|
my_client_id = models.CharField(
|
||||||
|
max_length=24,
|
||||||
|
)
|
||||||
|
my_client_secret = models.CharField(
|
||||||
|
max_length=48,
|
||||||
|
)
|
||||||
|
response_uri = models.URLField(
|
||||||
|
help_text=_("Url where to send the verificable presentation"),
|
||||||
|
max_length=250
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(self, vp, code):
|
||||||
|
"""
|
||||||
|
Send the verificable presentation to Verifier
|
||||||
|
"""
|
||||||
|
url = "{url}/verify".format(
|
||||||
|
url=self.response_uri.strip("/"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Send the a request for start a process of Verifier
|
||||||
|
"""
|
||||||
|
url = "{url}/verify?demand_uri={redirect_uri}".format(
|
||||||
|
url=self.response_uri.strip("/"),
|
||||||
|
redirect_uri=settings.RESPONSE_URI
|
||||||
|
)
|
||||||
|
auth = (self.my_client_id, self.my_client_secret)
|
||||||
|
return requests.get(url, auth=auth)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
###################
|
||||||
|
# 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)
|
||||||
|
code_used = models.BooleanField(default=False)
|
||||||
|
created = models.DateTimeField(auto_now=True)
|
||||||
|
presentation_definition = models.CharField(max_length=250)
|
||||||
|
organization = models.ForeignKey(
|
||||||
|
Organization,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='authorizations',
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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=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)
|
||||||
|
result_verify = models.CharField(max_length=255)
|
||||||
|
vp_token = models.TextField()
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
authorization = models.ForeignKey(
|
||||||
|
Authorization,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='vp_tokens',
|
||||||
|
null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
code = kwargs.pop("code", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.authorization = Authorization.objects.filter(code=code).first()
|
||||||
|
|
||||||
|
def verifing(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
11
oidc4vp/templates/credentials/verifiable_presentation.json
Normal file
11
oidc4vp/templates/credentials/verifiable_presentation.json
Normal file
|
@ -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|safe }}
|
||||||
|
}
|
96
oidc4vp/templates/credentials_presentation.html
Normal file
96
oidc4vp/templates/credentials_presentation.html
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
{% extends "idhub/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h3>
|
||||||
|
<i class="{{ icon }}"></i>
|
||||||
|
{{ subtitle }}
|
||||||
|
</h3>
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
<form role="form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% if form.errors %}
|
||||||
|
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||||
|
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||||
|
<div class="message">
|
||||||
|
{% for field, error in form.errors.items %}
|
||||||
|
{{ error }}<br />
|
||||||
|
{% endfor %}
|
||||||
|
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.credentials.all %}
|
||||||
|
{% for presentation in form.presentation_definition %}
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col">
|
||||||
|
<h3>{{ presentation|capfirst }}</3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col"></th>
|
||||||
|
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Type' %}</button></th>
|
||||||
|
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Details' %}</button></th>
|
||||||
|
<th scope="col"><button type="button" class="btn btn-grey border border-dark">{% trans 'Issued' %}</button></th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for f in form.credentials.all %}
|
||||||
|
{% if f.schema.type.lower == presentation.lower %}
|
||||||
|
<tr style="font-size:15px;">
|
||||||
|
<td><input class="form-check-input" type="radio" value="{{ f.id }}" name="{{ presentation.lower }}"></td>
|
||||||
|
<td>{{ f.type }}</td>
|
||||||
|
<td>{{ f.description }}</td>
|
||||||
|
<td>{{ f.get_issued_on }}</td>
|
||||||
|
<td><a href="{% url 'idhub:user_credential' f.id %}" class="text-primary" title="{% trans 'View' %}"><i class="bi bi-eye"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="form-actions-no-box mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" name="consent" required="required" /> {% trans 'I read and understood the' %}
|
||||||
|
<a href="javascript:void()" data-bs-toggle="modal" data-bs-target="#legality-consent">{% trans 'data sharing notice' %}</a>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-no-box mt-5">
|
||||||
|
<a class="btn btn-grey" href="{% url 'idhub:user_demand_authorization' %}">{% trans "Cancel" %}</a>
|
||||||
|
<input class="btn btn-green-user" type="submit" name="submit" value="{% trans 'Present' %}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col">
|
||||||
|
<h3>{% trans 'There are not credentials for present' %}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal" id="legality-consent" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">{% trans 'Data sharing notice' %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{% trans 'Are you sure that you want delete this user?' %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
3
oidc4vp/tests.py
Normal file
3
oidc4vp/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
16
oidc4vp/urls.py
Normal file
16
oidc4vp/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django.urls import path, reverse_lazy
|
||||||
|
|
||||||
|
from oidc4vp import views
|
||||||
|
|
||||||
|
|
||||||
|
app_name = 'oidc4vp'
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('verify', views.VerifyView.as_view(),
|
||||||
|
name="verify"),
|
||||||
|
path('authorize', views.AuthorizeView.as_view(),
|
||||||
|
name="authorize"),
|
||||||
|
path('allow_code', views.AllowCodeView.as_view(),
|
||||||
|
name="allow_code"),
|
||||||
|
]
|
165
oidc4vp/views.py
Normal file
165
oidc4vp/views.py
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.views.generic.edit import View, FormView
|
||||||
|
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
|
||||||
|
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, OAuth2VPToken
|
||||||
|
from idhub.mixins import UserView
|
||||||
|
|
||||||
|
from oidc4vp.forms import AuthorizeForm
|
||||||
|
from utils.idhub_ssikit import verify_presentation
|
||||||
|
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
try:
|
||||||
|
vps = json.loads(self.request.GET.get('presentation_definition'))
|
||||||
|
except:
|
||||||
|
vps = []
|
||||||
|
kwargs['presentation_definition'] = vps
|
||||||
|
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()
|
||||||
|
if not authorization or authorization.status_code != 200:
|
||||||
|
messages.error(self.request, _("Error sending credential!"))
|
||||||
|
return super().form_valid(form)
|
||||||
|
try:
|
||||||
|
authorization = authorization.json()
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name='dispatch')
|
||||||
|
class VerifyView(View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
org = self.validate(request)
|
||||||
|
presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS)
|
||||||
|
authorization = Authorization(
|
||||||
|
organization=org,
|
||||||
|
presentation_definition=presentation_definition
|
||||||
|
)
|
||||||
|
authorization.save()
|
||||||
|
res = json.dumps({"redirect_uri": authorization.authorize()})
|
||||||
|
return HttpResponse(res)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if not vp_token.authorization:
|
||||||
|
raise Http404("Page not Found!")
|
||||||
|
|
||||||
|
vp_token.verifing()
|
||||||
|
response = vp_token.get_response_verify()
|
||||||
|
vp_token.save()
|
||||||
|
if not vp_token.authorization.promotions.exists():
|
||||||
|
response["redirect_uri"] = ""
|
||||||
|
response["response"] = "Validation Code {}".format(code)
|
||||||
|
|
||||||
|
return JsonResponse(response)
|
||||||
|
|
||||||
|
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() == '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,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret
|
||||||
|
)
|
||||||
|
return org
|
||||||
|
|
||||||
|
raise Http404("Page not Found!")
|
||||||
|
|
||||||
|
|
||||||
|
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.exists():
|
||||||
|
raise Http404("Page not Found!")
|
||||||
|
|
||||||
|
promotion = self.authorization.promotions.all()[0]
|
||||||
|
return redirect(promotion.get_url(code))
|
||||||
|
|
0
promotion/__init__.py
Normal file
0
promotion/__init__.py
Normal file
3
promotion/admin.py
Normal file
3
promotion/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
promotion/apps.py
Normal file
6
promotion/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PromotionConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'promotion'
|
65
promotion/forms.py
Normal file
65
promotion/forms.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
|
||||||
|
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, Authorization
|
||||||
|
from promotion.models import Promotion
|
||||||
|
|
||||||
|
|
||||||
|
class WalletForm(forms.Form):
|
||||||
|
organization = forms.ChoiceField(choices=[])
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.presentation_definition = kwargs.pop('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]
|
||||||
|
|
||||||
|
self.authorization = Authorization(
|
||||||
|
organization=self.org,
|
||||||
|
presentation_definition=self.presentation_definition,
|
||||||
|
)
|
||||||
|
self.promotion = Promotion(
|
||||||
|
discount = Promotion.Types.VULNERABLE.value,
|
||||||
|
authorize = self.authorization
|
||||||
|
)
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
self.authorization.save()
|
||||||
|
self.promotion.save()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
45
promotion/migrations/0001_initial.py
Normal file
45
promotion/migrations/0001_initial.py
Normal file
|
@ -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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
promotion/migrations/__init__.py
Normal file
0
promotion/migrations/__init__.py
Normal file
31
promotion/models.py
Normal file
31
promotion/models.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from oidc4vp.models import Authorization
|
||||||
|
|
||||||
|
|
||||||
|
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:contract"),
|
||||||
|
code
|
||||||
|
)
|
||||||
|
return url
|
||||||
|
|
||||||
|
def get_discount(self, price):
|
||||||
|
return price - price*0.25
|
||||||
|
|
1126
promotion/templates/select_wallet.html
Normal file
1126
promotion/templates/select_wallet.html
Normal file
File diff suppressed because one or more lines are too long
1170
promotion/templates/somconnexio_contract.html
Normal file
1170
promotion/templates/somconnexio_contract.html
Normal file
File diff suppressed because one or more lines are too long
1219
promotion/templates/somconnexio_tarifes_mobil.html
Normal file
1219
promotion/templates/somconnexio_tarifes_mobil.html
Normal file
File diff suppressed because one or more lines are too long
1223
promotion/templates/somconnexio_thanks.html
Normal file
1223
promotion/templates/somconnexio_thanks.html
Normal file
File diff suppressed because one or more lines are too long
3
promotion/tests.py
Normal file
3
promotion/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
18
promotion/urls.py
Normal file
18
promotion/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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"),
|
||||||
|
path('contract', views.ContractView.as_view(),
|
||||||
|
name="contract"),
|
||||||
|
path('contract/1', views.ThanksView.as_view(),
|
||||||
|
name="thanks"),
|
||||||
|
]
|
125
promotion/views.py
Normal file
125
promotion/views.py
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
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 oidc4vp.models import Authorization
|
||||||
|
from promotion.forms import WalletForm, ContractForm
|
||||||
|
|
||||||
|
|
||||||
|
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 ThanksView(View):
|
||||||
|
template_name = "somconnexio_thanks.html"
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.context = {}
|
||||||
|
template = get_template(
|
||||||
|
self.template_name,
|
||||||
|
).render()
|
||||||
|
return HttpResponse(template)
|
||||||
|
|
||||||
|
|
||||||
|
class ContractView(FormView):
|
||||||
|
template_name = "somconnexio_contract.html"
|
||||||
|
promotion = None
|
||||||
|
vp_token = None
|
||||||
|
authorization = None
|
||||||
|
form_class = ContractForm
|
||||||
|
success_url = reverse_lazy('promotion:thanks')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
self.context = super().get_context_data(**kwargs)
|
||||||
|
code = self.request.GET.get("code")
|
||||||
|
self.get_discount(code)
|
||||||
|
self.context.update({
|
||||||
|
"promotion": self.promotion,
|
||||||
|
"verificable_presentation": self.vp_token,
|
||||||
|
"sim": 10.0,
|
||||||
|
"mensual": 15.0,
|
||||||
|
"total": 25.0
|
||||||
|
})
|
||||||
|
if self.promotion:
|
||||||
|
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['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):
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_discount(self, code):
|
||||||
|
if not code:
|
||||||
|
return
|
||||||
|
if self.authorization:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.authorization = Authorization.objects.filter(
|
||||||
|
code=code,
|
||||||
|
code_used=False
|
||||||
|
).first()
|
||||||
|
if self.authorization:
|
||||||
|
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):
|
||||||
|
template_name = "select_wallet.html"
|
||||||
|
form_class = WalletForm
|
||||||
|
success_url = reverse_lazy('promotion:select_wallet')
|
||||||
|
# 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)
|
||||||
|
|
|
@ -12,3 +12,5 @@ jinja2==3.1.2
|
||||||
jsonref==1.1.0
|
jsonref==1.1.0
|
||||||
pyld==2.0.3
|
pyld==2.0.3
|
||||||
pynacl==1.5.0
|
pynacl==1.5.0
|
||||||
|
more-itertools==10.1.0
|
||||||
|
dj-database-url==2.1.0
|
||||||
|
|
|
@ -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"]
|
|
||||||
}
|
|
|
@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
from dj_database_url import parse as db_url
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
@ -72,13 +73,16 @@ INSTALLED_APPS = [
|
||||||
'django_bootstrap5',
|
'django_bootstrap5',
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
'idhub_auth',
|
'idhub_auth',
|
||||||
'idhub'
|
'oidc4vp',
|
||||||
|
'idhub',
|
||||||
|
'promotion'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
@ -110,10 +114,15 @@ WSGI_APPLICATION = 'trustchain_idhub.wsgi.application'
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
# 'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
}
|
# }
|
||||||
|
'default': config(
|
||||||
|
'DATABASE_URL',
|
||||||
|
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
cast=db_url
|
||||||
|
)
|
||||||
# 'default': config(
|
# 'default': config(
|
||||||
# 'DATABASE_URL',
|
# 'DATABASE_URL',
|
||||||
# default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
# default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
@ -178,9 +187,38 @@ MESSAGE_TAGS = {
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
os.path.join(BASE_DIR, 'locale'),
|
os.path.join(BASE_DIR, 'locale'),
|
||||||
]
|
]
|
||||||
LANGUAGE_CODE="en"
|
# LANGUAGE_CODE="en"
|
||||||
# LANGUAGE_CODE="es"
|
# LANGUAGE_CODE="es"
|
||||||
|
LANGUAGE_CODE="ca"
|
||||||
|
gettext = lambda s: s
|
||||||
|
LANGUAGES = (
|
||||||
|
('de', gettext('German')),
|
||||||
|
('en', gettext('English')),
|
||||||
|
('ca', gettext('Catalan')),
|
||||||
|
)
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_L10N = True
|
USE_L10N = True
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'idhub_auth.User'
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"console": {"class": "logging.StreamHandler"},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,4 +24,6 @@ from django.contrib.auth import views as auth_views
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('django-admin/', admin.site.urls),
|
# path('django-admin/', admin.site.urls),
|
||||||
path('', include('idhub.urls')),
|
path('', include('idhub.urls')),
|
||||||
|
path('oidc4vp/', include('oidc4vp.urls')),
|
||||||
|
path('promotion/', include('promotion.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
```
|
|
@ -4,6 +4,7 @@ import didkit
|
||||||
import json
|
import json
|
||||||
import jinja2
|
import jinja2
|
||||||
from django.template.backends.django import Template
|
from django.template.backends.django import Template
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
|
||||||
def generate_did_controller_key():
|
def generate_did_controller_key():
|
||||||
|
@ -49,7 +50,8 @@ def render_and_sign_credential(vc_template: jinja2.Template, jwk_issuer, vc_data
|
||||||
|
|
||||||
def sign_credential(unsigned_vc: str, jwk_issuer):
|
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():
|
async def inner():
|
||||||
signed_vc = await didkit.issue_credential(
|
signed_vc = await didkit.issue_credential(
|
||||||
|
@ -62,13 +64,57 @@ def sign_credential(unsigned_vc: str, jwk_issuer):
|
||||||
return asyncio.run(inner())
|
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.
|
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 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.
|
If it is false, the VC is invalid and the second argument contains a JSON object with further information.
|
||||||
"""
|
"""
|
||||||
async def inner():
|
async def inner():
|
||||||
return didkit.verify_credential(vc, proof_options)
|
return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}')
|
||||||
|
|
||||||
return asyncio.run(inner())
|
return asyncio.run(inner())
|
||||||
|
|
||||||
|
|
||||||
|
def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str:
|
||||||
|
async def inner():
|
||||||
|
unsigned_vp = vp_template.render(data)
|
||||||
|
signed_vp = await didkit.issue_presentation(
|
||||||
|
unsigned_vp,
|
||||||
|
'{"proofFormat": "ldp"}',
|
||||||
|
jwk_holder
|
||||||
|
)
|
||||||
|
return signed_vp
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"holder_did": holder_did,
|
||||||
|
"verifiable_credential_list": "[" + ",".join(vc_list) + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
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 await didkit.verify_presentation(vp, proof_options)
|
||||||
|
|
||||||
|
return asyncio.run(inner())
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue