Merge branch 'oidc4vp'
This commit is contained in:
commit
2ef26ab9a2
|
@ -1,2 +1,5 @@
|
|||
"ExO";"https://verify.exo.cat"
|
||||
"Somos Connexión";"https://verify.somosconexion.coop"
|
||||
"pangea.org";"https://idhub1.demo.pangea.org/oidc4vp/"
|
||||
"somconnexio.coop";"https://idhub2.demo.pangea.org/oidc4vp/"
|
||||
"exo.cat";"https://verify.exo.cat"
|
||||
"local 9000";"http://localhost:9000/oidc4vp/"
|
||||
"local 8000";"http://localhost:8000/oidc4vp/"
|
||||
|
|
|
|
@ -629,7 +629,7 @@ class DidsView(Credentials):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'dids': DID.objects,
|
||||
'dids': DID.objects.filter(user=self.request.user),
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -824,7 +824,6 @@ class SchemasImportAddView(SchemasMix):
|
|||
assert credtools.validate_schema(ldata)
|
||||
name = ldata.get('name')
|
||||
assert name
|
||||
|
||||
except Exception:
|
||||
messages.error(self.request, _('This is not a valid schema!'))
|
||||
return
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import os
|
||||
import csv
|
||||
import json
|
||||
|
||||
from pathlib import Path
|
||||
from utils import credtools
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth import get_user_model
|
||||
from decouple import config
|
||||
from idhub.models import Organization
|
||||
from idhub.models import DID, Schemas
|
||||
from oidc4vp.models import Organization
|
||||
from promotion.models import Promotion
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -29,6 +34,10 @@ class Command(BaseCommand):
|
|||
f = csv.reader(csvfile, delimiter=';', quotechar='"')
|
||||
for r in f:
|
||||
self.create_organizations(r[0].strip(), r[1].strip())
|
||||
self.sync_credentials_organizations("pangea.org", "somconnexio.coop")
|
||||
self.sync_credentials_organizations("local 8000", "local 9000")
|
||||
self.create_defaults_dids()
|
||||
self.create_schemas()
|
||||
|
||||
def create_admin_users(self, email, password):
|
||||
User.objects.create_superuser(email=email, password=password)
|
||||
|
@ -41,4 +50,48 @@ class Command(BaseCommand):
|
|||
|
||||
|
||||
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 2023-12-01 16:40
|
||||
# Generated by Django 4.2.5 on 2023-12-11 08:35
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
@ -57,27 +57,6 @@ class Migration(migrations.Migration):
|
|||
('created_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name='ID',
|
||||
),
|
||||
),
|
||||
('name', models.CharField(max_length=250)),
|
||||
(
|
||||
'url',
|
||||
models.CharField(
|
||||
help_text='Url where to send the presentation', max_length=250
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Rol',
|
||||
fields=[
|
||||
|
@ -169,7 +148,6 @@ class Migration(migrations.Migration):
|
|||
('verified', models.BooleanField()),
|
||||
('created_on', models.DateTimeField(auto_now=True)),
|
||||
('issued_on', models.DateTimeField(null=True)),
|
||||
('subject_did', models.CharField(max_length=250)),
|
||||
('data', models.TextField()),
|
||||
('csv_data', models.TextField()),
|
||||
(
|
||||
|
@ -200,6 +178,15 @@ class Migration(migrations.Migration):
|
|||
to='idhub.schemas',
|
||||
),
|
||||
),
|
||||
(
|
||||
'subject_did',
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='subject_credentials',
|
||||
to='idhub.did',
|
||||
),
|
||||
),
|
||||
(
|
||||
'user',
|
||||
models.ForeignKey(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
import pytz
|
||||
import requests
|
||||
import datetime
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
@ -464,7 +463,6 @@ class VerificableCredential(models.Model):
|
|||
verified = models.BooleanField()
|
||||
created_on = models.DateTimeField(auto_now=True)
|
||||
issued_on = models.DateTimeField(null=True)
|
||||
subject_did = models.CharField(max_length=250)
|
||||
data = models.TextField()
|
||||
csv_data = models.TextField()
|
||||
status = models.PositiveSmallIntegerField(
|
||||
|
@ -476,6 +474,12 @@ class VerificableCredential(models.Model):
|
|||
on_delete=models.CASCADE,
|
||||
related_name='vcredentials',
|
||||
)
|
||||
subject_did = models.ForeignKey(
|
||||
DID,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='subject_credentials',
|
||||
null=True
|
||||
)
|
||||
issuer_did = models.ForeignKey(
|
||||
DID,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -525,7 +529,7 @@ class VerificableCredential(models.Model):
|
|||
context = {
|
||||
'vc_id': self.id,
|
||||
'issuer_did': self.issuer_did.did,
|
||||
'subject_did': self.subject_did,
|
||||
'subject_did': self.subject_did and self.subject_did.did or '',
|
||||
'issuance_date': issuance_date,
|
||||
'first_name': self.user.first_name,
|
||||
'last_name': self.user.last_name,
|
||||
|
@ -630,18 +634,3 @@ class UserRol(models.Model):
|
|||
|
||||
class Meta:
|
||||
unique_together = ('user', 'service',)
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
name = models.CharField(max_length=250)
|
||||
url = models.CharField(
|
||||
help_text=_("Url where to send the presentation"),
|
||||
max_length=250
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def send(self, cred):
|
||||
return
|
||||
requests.post(self.url, data=cred.data)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,7 +115,7 @@
|
|||
</a>
|
||||
</li>
|
||||
<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' %}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy
|
|||
from .views import LoginView
|
||||
from .admin import views as views_admin
|
||||
from .user import views as views_user
|
||||
# from .verification_portal import views as views_verification_portal
|
||||
|
||||
app_name = 'idhub'
|
||||
|
||||
|
@ -84,6 +85,9 @@ urlpatterns = [
|
|||
path('user/credentials/request/',
|
||||
views_user.CredentialsRequestView.as_view(),
|
||||
name='user_credentials_request'),
|
||||
path('user/credentials_presentation/demand',
|
||||
views_user.DemandAuthorizationView.as_view(),
|
||||
name='user_demand_authorization'),
|
||||
path('user/credentials_presentation/',
|
||||
views_user.CredentialsPresentationView.as_view(),
|
||||
name='user_credentials_presentation'),
|
||||
|
@ -171,4 +175,7 @@ urlpatterns = [
|
|||
name='admin_import'),
|
||||
path('admin/import/new', views_admin.ImportAddView.as_view(),
|
||||
name='admin_import_add'),
|
||||
|
||||
# path('verification_portal/verify/', views_verification_portal.verify,
|
||||
# name="verification_portal_verify")
|
||||
]
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import requests
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from idhub_auth.models import User
|
||||
from idhub.models import DID, VerificableCredential, Organization
|
||||
|
||||
from idhub.models import DID, VerificableCredential
|
||||
from oidc4vp.models import Organization
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
|
@ -42,7 +44,7 @@ class RequestCredentialForm(forms.Form):
|
|||
if not all([cred.exists(), did.exists()]):
|
||||
return
|
||||
|
||||
did = did[0].did
|
||||
did = did[0]
|
||||
cred = cred[0]
|
||||
try:
|
||||
cred.issue(did)
|
||||
|
@ -56,10 +58,37 @@ class RequestCredentialForm(forms.Form):
|
|||
return
|
||||
|
||||
|
||||
class DemandAuthorizationForm(forms.Form):
|
||||
organization = forms.ChoiceField(choices=[])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['organization'].choices = [
|
||||
(x.id, x.name) for x in Organization.objects.filter()
|
||||
if x.response_uri != settings.RESPONSE_URI
|
||||
]
|
||||
|
||||
def save(self, commit=True):
|
||||
self.org = Organization.objects.filter(
|
||||
id=self.data['organization']
|
||||
)
|
||||
if not self.org.exists():
|
||||
return
|
||||
|
||||
self.org = self.org[0]
|
||||
|
||||
if commit:
|
||||
url = self.org.demand_authorization()
|
||||
if url.status_code == 200:
|
||||
return url.json().get('redirect_uri')
|
||||
|
||||
return
|
||||
|
||||
|
||||
class CredentialPresentationForm(forms.Form):
|
||||
organization = forms.ChoiceField(choices=[])
|
||||
credential = forms.ChoiceField(choices=[])
|
||||
# credential = forms.ChoiceField(choices=[])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user', None)
|
||||
|
@ -67,12 +96,12 @@ class CredentialPresentationForm(forms.Form):
|
|||
self.fields['organization'].choices = [
|
||||
(x.id, x.name) for x in Organization.objects.filter()
|
||||
]
|
||||
self.fields['credential'].choices = [
|
||||
(x.id, x.type()) for x in VerificableCredential.objects.filter(
|
||||
user=self.user,
|
||||
status=VerificableCredential.Status.ISSUED
|
||||
)
|
||||
]
|
||||
# self.fields['credential'].choices = [
|
||||
# (x.id, x.type()) for x in VerificableCredential.objects.filter(
|
||||
# user=self.user,
|
||||
# status=VerificableCredential.Status.ISSUED
|
||||
# )
|
||||
# ]
|
||||
|
||||
def save(self, commit=True):
|
||||
self.org = Organization.objects.filter(
|
||||
|
|
|
@ -12,7 +12,12 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.urls import reverse_lazy
|
||||
from django.http import HttpResponse
|
||||
from django.contrib import messages
|
||||
from idhub.user.forms import ProfileForm, RequestCredentialForm, CredentialPresentationForm
|
||||
from idhub.user.forms import (
|
||||
ProfileForm,
|
||||
RequestCredentialForm,
|
||||
CredentialPresentationForm,
|
||||
DemandAuthorizationForm
|
||||
)
|
||||
from idhub.mixins import UserView
|
||||
from idhub.models import DID, VerificableCredential, Event
|
||||
|
||||
|
@ -76,8 +81,11 @@ class CredentialsView(MyWallet, TemplateView):
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
creds = VerificableCredential.objects.filter(
|
||||
user=self.request.user
|
||||
)
|
||||
context.update({
|
||||
'credentials': VerificableCredential.objects,
|
||||
'credentials': creds,
|
||||
})
|
||||
return context
|
||||
|
||||
|
@ -136,11 +144,43 @@ class CredentialsRequestView(MyWallet, FormView):
|
|||
messages.success(self.request, _("The credential was issued successfully!"))
|
||||
Event.set_EV_CREDENTIAL_ISSUED_FOR_USER(cred)
|
||||
Event.set_EV_CREDENTIAL_ISSUED(cred)
|
||||
url = self.request.session.pop('next_url', None)
|
||||
if url:
|
||||
return redirect(url)
|
||||
else:
|
||||
messages.error(self.request, _("The credential does not exist!"))
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DemandAuthorizationView(MyWallet, FormView):
|
||||
template_name = "idhub/user/credentials_presentation.html"
|
||||
subtitle = _('Credential presentation')
|
||||
icon = 'bi bi-patch-check-fill'
|
||||
form_class = DemandAuthorizationForm
|
||||
success_url = reverse_lazy('idhub:user_demand_authorization')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
authorization = form.save()
|
||||
except Exception:
|
||||
txt = _("Problems connecting with {url}").format(
|
||||
url=form.org.response_uri
|
||||
)
|
||||
messages.error(self.request, txt)
|
||||
return super().form_valid(form)
|
||||
|
||||
if authorization:
|
||||
return redirect(authorization)
|
||||
else:
|
||||
messages.error(self.request, _("Error sending credential!"))
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class CredentialsPresentationView(MyWallet, FormView):
|
||||
template_name = "idhub/user/credentials_presentation.html"
|
||||
subtitle = _('Credential presentation')
|
||||
|
@ -151,6 +191,7 @@ class CredentialsPresentationView(MyWallet, FormView):
|
|||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
kwargs['authorize'] = self.request.GET.params.get("uri")
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 4.2.5 on 2023-12-01 16:40
|
||||
# Generated by Django 4.2.5 on 2023-12-11 08:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Oidc4VpConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'oidc4vp'
|
|
@ -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)
|
|
@ -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,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")
|
|
@ -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 }}
|
||||
}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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"),
|
||||
]
|
|
@ -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,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PromotionConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'promotion'
|
|
@ -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()
|
||||
|
|
@ -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,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
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -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"),
|
||||
]
|
|
@ -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)
|
||||
|
|
@ -11,3 +11,5 @@ didkit==0.3.2
|
|||
jinja2==3.1.2
|
||||
jsonref==1.1.0
|
||||
pyld==2.0.3
|
||||
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
|
||||
|
||||
from ast import literal_eval
|
||||
from dj_database_url import parse as db_url
|
||||
|
||||
from pathlib import Path
|
||||
from django.contrib.messages import constants as messages
|
||||
|
@ -72,7 +73,9 @@ INSTALLED_APPS = [
|
|||
'django_bootstrap5',
|
||||
'django_tables2',
|
||||
'idhub_auth',
|
||||
'idhub'
|
||||
'oidc4vp',
|
||||
'idhub',
|
||||
'promotion'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -111,10 +114,15 @@ WSGI_APPLICATION = 'trustchain_idhub.wsgi.application'
|
|||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# }
|
||||
'default': config(
|
||||
'DATABASE_URL',
|
||||
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
cast=db_url
|
||||
)
|
||||
# 'default': config(
|
||||
# 'DATABASE_URL',
|
||||
# default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
|
@ -179,9 +187,17 @@ MESSAGE_TAGS = {
|
|||
LOCALE_PATHS = [
|
||||
os.path.join(BASE_DIR, 'locale'),
|
||||
]
|
||||
LANGUAGE_CODE="en"
|
||||
# LANGUAGE_CODE="en"
|
||||
# LANGUAGE_CODE="es"
|
||||
LANGUAGE_CODE="ca"
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
|
||||
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
|
||||
)
|
||||
|
|
|
@ -24,4 +24,6 @@ from django.contrib.auth import views as auth_views
|
|||
urlpatterns = [
|
||||
# path('django-admin/', admin.site.urls),
|
||||
path('', include('idhub.urls')),
|
||||
path('oidc4vp/', include('oidc4vp.urls')),
|
||||
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 jinja2
|
||||
from django.template.backends.django import Template
|
||||
from django.template.loader import get_template
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Signs the and unsigned credential with the provided key.
|
||||
Signs the unsigned credential with the provided key.
|
||||
The credential template must be rendered with all user data.
|
||||
"""
|
||||
async def inner():
|
||||
signed_vc = await didkit.issue_credential(
|
||||
|
@ -62,13 +64,57 @@ def sign_credential(unsigned_vc: str, jwk_issuer):
|
|||
return asyncio.run(inner())
|
||||
|
||||
|
||||
def verify_credential(vc, proof_options):
|
||||
def verify_credential(vc):
|
||||
"""
|
||||
Returns a (bool, str) tuple indicating whether the credential is valid.
|
||||
If the boolean is true, the credential is valid and the second argument can be ignored.
|
||||
If it is false, the VC is invalid and the second argument contains a JSON object with further information.
|
||||
"""
|
||||
async def inner():
|
||||
return didkit.verify_credential(vc, proof_options)
|
||||
return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}')
|
||||
|
||||
return asyncio.run(inner())
|
||||
|
||||
|
||||
def issue_verifiable_presentation(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 New Issue