From 6d9faec1affe849bb0db5d4beab2007ddf5b63fc Mon Sep 17 00:00:00 2001 From: Miguel Ortega Date: Wed, 19 Nov 2025 15:31:16 +0100 Subject: [PATCH 1/5] feat(login): add configuration to enable Sign In using VC --- src/requirements.txt | 2 + src/settings.py | 18 +- src/urls.py | 11 ++ src/wirecloud/backends/vc_backend.py | 73 ++++++++ src/wirecloud/commons/utils/conf.py | 1 + .../locale/es/LC_MESSAGES/django.po | 8 + .../templates/registration/login.html | 15 +- src/wirecloud/platform/context_processors.py | 16 +- src/wirecloud/vc_login/exceptions.py | 25 +++ src/wirecloud/vc_login/vc_login.py | 172 ++++++++++++++++++ src/wirecloud/vc_login/vc_payload.py | 16 ++ src/wirecloud/vc_login/views.py | 80 ++++++++ 12 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 src/wirecloud/backends/vc_backend.py create mode 100644 src/wirecloud/vc_login/exceptions.py create mode 100644 src/wirecloud/vc_login/vc_login.py create mode 100644 src/wirecloud/vc_login/vc_payload.py create mode 100644 src/wirecloud/vc_login/views.py diff --git a/src/requirements.txt b/src/requirements.txt index 60a244ccd6..c3ff5ea653 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ Django>=2.0,<2.3 +django-nose lxml>=2.3 django_compressor>=2.0,<2.3 rdflib>=3.2.0 @@ -15,3 +16,4 @@ pyScss>=1.3.4,<2.0 Pygments pillow jsonpatch +python-jose>=3.3.0 \ No newline at end of file diff --git a/src/settings.py b/src/settings.py index 1ece504c45..2b7ef24c90 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Django settings used as base for developing wirecloud. -from os import path +from os import path, environ from wirecloud.commons.utils.conf import load_default_wirecloud_conf from django.urls import reverse_lazy @@ -116,7 +116,7 @@ # Authentication AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', + 'django.contrib.auth.backends.ModelBackend' ) # WGT deployment dirs @@ -169,3 +169,17 @@ # 'CLASS': 'selenium.webdriver.Safari', # }, # } + + +VC_LOGIN_CONFIG = { + 'enabled': environ.get('VC_LOGIN_ENABLED', 'True').lower() in ('true', 'yes', 't'), + 'verifier_host': environ.get('VC_VERIFIER_HOST', 'https://verifier.seamware.io'), + 'verifier_qr_path': environ.get('VC_VERIFIER_QR_PATH', '/api/v2/loginQR'), + 'verifier_token_path': environ.get('VC_VERIFIER_TOKEN_PATH', '/token'), + 'verifier_jwks_path': environ.get('VC_VERIFIER_JWKS_PATH', '/.well-known/jwks'), + 'client_id': environ.get('VC_CLIENT_ID', 'did:web:did-producer.seamware.io:did'), + 'scope': environ.get('VC_SCOPE', 'openid learcredential') +} + +if VC_LOGIN_CONFIG['enabled']: + AUTHENTICATION_BACKENDS += ('wirecloud.backends.vc_backend.VCBackend') \ No newline at end of file diff --git a/src/urls.py b/src/urls.py index 5196e6bebb..64ffe09498 100644 --- a/src/urls.py +++ b/src/urls.py @@ -5,10 +5,16 @@ from django.contrib import admin from django.contrib.auth import views as django_auth from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.conf import settings from wirecloud.commons import authentication as wc_auth import wirecloud.platform.urls + +vc_login_enabled = settings.VC_LOGIN_CONFIG['enabled'] +if vc_login_enabled: + from wirecloud.vc_login import views as vc_login_views + admin.autodiscover() urlpatterns = ( @@ -23,6 +29,11 @@ url(r'^login/?$', django_auth.LoginView.as_view(), name="login"), url(r'^logout/?$', wc_auth.logout, name="logout"), url(r'^admin/logout/?$', wc_auth.logout), + # VC login when enabled + *([ + url(r'^vc/login/?$', vc_login_views.vc_sso_login, name='vc_sso_login'), + url(r'^vc/callback/?$', vc_login_views.vc_sso_callback, name='vc_sso_callback'), + ] if vc_login_enabled else []), # Admin interface url(r'^admin/', admin.site.urls), diff --git a/src/wirecloud/backends/vc_backend.py b/src/wirecloud/backends/vc_backend.py new file mode 100644 index 0000000000..da0c4163be --- /dev/null +++ b/src/wirecloud/backends/vc_backend.py @@ -0,0 +1,73 @@ +from django.contrib.auth.backends import ModelBackend +from django.contrib.auth import get_user_model +from django.db import IntegrityError +import logging +from wirecloud.vc_login.vc_payload import VCPayload +# Get the active User model (handles custom user models) +User = get_user_model() +logger = logging.getLogger(__name__) + +class VCBackend(ModelBackend): + """ + Custom authentication backend that provisions (creates) a new user or + logs in and updates an existing user based on a validated + Verifiable Credential (VC) payload. + """ + + def authenticate(self, request, vc_payload: VCPayload = None, **kwargs): + """ + Retrieves user data from the VC payload and handles the login/creation process. + """ + if vc_payload is None: + return None + + email = vc_payload.email + first_name = vc_payload.first_name + last_name = vc_payload.last_name + if not email: + logger.error("VC Payload missing required 'email' field for authentication.") + return None + + try: + user = User.objects.get(email=email) + + is_updated = False + + if user.first_name != first_name: + user.first_name = first_name + is_updated = True + if user.last_name != last_name: + user.last_name = last_name + is_updated = True + if is_updated: + user.save() + + logger.info(f"Existing user logged in successfully: {email}") + return user + + except User.DoesNotExist: + try: + logger.info(f"User not found. Creating new user: {email}") + user = User.objects.create_user( + username=email, + email=email, + first_name=first_name, + last_name=last_name + ) + logger.info(f"New user provisioned: {user.username}") + return user + + except IntegrityError: + logger.warning(f"Integrity conflict during user creation for {email}.") + return None + except Exception as e: + logger.error(f"Unexpected error during user creation: {e}") + return None + + + def get_user(self, user_id): + """Required method for Django session management.""" + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None \ No newline at end of file diff --git a/src/wirecloud/commons/utils/conf.py b/src/wirecloud/commons/utils/conf.py index 53d3bec5c1..82a3a325af 100644 --- a/src/wirecloud/commons/utils/conf.py +++ b/src/wirecloud/commons/utils/conf.py @@ -106,6 +106,7 @@ def load_default_wirecloud_conf(settings, instance_type='platform'): 'django.contrib.messages.context_processors.messages', 'wirecloud.platform.context_processors.plugins', 'wirecloud.platform.context_processors.active_theme', + 'wirecloud.platform.context_processors.vc_login_context', ), 'debug': settings['DEBUG'], 'loaders': ( diff --git a/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po b/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po index 601805e01b..2fa5cefbc3 100644 --- a/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po +++ b/src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po @@ -389,3 +389,11 @@ msgstr "" "
  • Instalar otra versión del widget y usar la opción " "Actualizar/Desactualizar
  • \n" " " + +#: templates/registration/login.html:54 +msgid "Use your digital wallet to authenticate with SSO" +msgstr "Usa tu wallet digital para autenticarte con SSO" + +#: templates/registration/login.html:55 +msgid "Sign In with Verificable Credentials" +msgstr "Iniciar sesión con Credenciales verificables" \ No newline at end of file diff --git a/src/wirecloud/defaulttheme/templates/registration/login.html b/src/wirecloud/defaulttheme/templates/registration/login.html index ed1b8b110a..01513cb308 100644 --- a/src/wirecloud/defaulttheme/templates/registration/login.html +++ b/src/wirecloud/defaulttheme/templates/registration/login.html @@ -19,7 +19,6 @@
    - {% with wc_logo_id="123456"|make_list|random %}{% with wc_logo="images/logos/wc"|add:wc_logo_id|add:".png" %} {% endwith %}{% endwith %} @@ -42,17 +41,23 @@

    {% trans "Your browser seems to lack some required features" %}

    {% csrf_token %} - +
    -
    +
    - +
    + {% if VC_LOGIN_ENABLED %} + + + {% trans 'Sign In with Verificable Credentials' %} + +
    - + {% endif %} - + @@ -25,47 +23,72 @@
    - + -
    -
    + +
    -
    +
    - {% if form.errors %} -
    {% blocktrans %}

    Your username and password didn't match.

    Please try again.

    {% endblocktrans %}
    - {% endif %} + {% if form.errors %} +
    {% blocktrans %}

    Your username and password didn't match. +

    +

    Please try again.

    {% endblocktrans %} +
    + {% endif %} - {% csrf_token %} - - -
    -
    -
    + {% csrf_token %} + + +
    +
    +
    +
    +
    + + -
    - - -
    - {% endif %} -
    - + - + + \ No newline at end of file