Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docker-platform/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,16 @@

from wirecloud.glogger import config as LOGGING

# MIDDLEWARE = ('django.middleware.gzip.GZipMiddleware',) + MIDDLEWARE
# MIDDLEWARE = ('django.middleware.gzip.GZipMiddleware',) + MIDDLEWARE

VC_LOGIN_CONFIG = {
'enabled': os.environ.get('VC_LOGIN_ENABLED', 'False').lower() in ('true', 'yes', 't'),
'verifier_host': os.environ.get('VC_VERIFIER_HOST'),
'verifier_qr_path': os.environ.get('VC_VERIFIER_QR_PATH', '/api/v2/loginQR'),
'verifier_token_path': os.environ.get('VC_VERIFIER_TOKEN_PATH', '/token'),
'verifier_jwks_path': os.environ.get('VC_VERIFIER_JWKS_PATH', '/.well-known/jwks'),
'client_id': os.environ.get('VC_CLIENT_ID'),
'scope': os.environ.get('VC_SCOPE', 'openid learcredential'),
'role_target': os.environ.get('VC_ROLE_TARGET'),
'credential_type': os.environ.get('VC_CREDENTIAL_TYPE', 'LegalPersonCredential'),
}
2 changes: 2 additions & 0 deletions src/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Django>=2.0,<2.3
django-nose
lxml>=2.3
django_compressor>=2.0,<2.3
rdflib>=3.2.0
Expand All @@ -15,3 +16,4 @@ pyScss>=1.3.4,<2.0
Pygments
pillow
jsonpatch
python-jose>=3.3.0
20 changes: 18 additions & 2 deletions src/settings.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -116,7 +116,7 @@

# Authentication
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'django.contrib.auth.backends.ModelBackend'
)

# WGT deployment dirs
Expand Down Expand Up @@ -169,3 +169,19 @@
# 'CLASS': 'selenium.webdriver.Safari',
# },
# }


VC_LOGIN_CONFIG = {
'enabled': environ.get('VC_LOGIN_ENABLED', 'False').lower() in ('true', 'yes', 't'),
'verifier_host': environ.get('VC_VERIFIER_HOST'),
'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'),
'scope': environ.get('VC_SCOPE', 'openid learcredential'),
'role_target': environ.get('VC_ROLE_TARGET'),
'credential_type': environ.get('VC_CREDENTIAL_TYPE', 'LegalPersonCredential'),
}

if VC_LOGIN_CONFIG['enabled']:
AUTHENTICATION_BACKENDS += ('wirecloud.backends.vc_backend.VCBackend')
11 changes: 11 additions & 0 deletions src/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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),
Expand Down
99 changes: 99 additions & 0 deletions src/wirecloud/backends/vc_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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
from django.contrib.auth.models import Group, AbstractUser
from django.conf import settings

# 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
user = None
roles = self._get_roles(vc_payload)
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}")

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}")

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
logger.info("User logged. Assign roles")
self._assign_roles(roles, user)
return user

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

def get_groups(self, groups):
return Group.objects.filter(name__in=groups)

def _assign_roles(self, user_roles, user: AbstractUser):
logger.info(f"User roles: {user_roles}")
allowed_groups = self.get_groups(user_roles)
if not allowed_groups:
logger.info("No allowed groups")
return
groups_to_add = allowed_groups.exclude(id__in=user.groups.values_list('id', flat=True))
if groups_to_add.exists():
groups = ", ".join([g.name for g in groups_to_add])
logger.info(f"Add user '{user.username}' to groups: {groups}")
user.groups.add(*groups_to_add)

def _get_roles(self, vc_payload: VCPayload):
client_id = settings.VC_LOGIN_CONFIG['role_target']
if not vc_payload.roles:
return []
return next((role['names'] for role in vc_payload.roles if role['target'] == client_id), [])
1 change: 1 addition & 0 deletions src/wirecloud/commons/utils/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': (
Expand Down
24 changes: 24 additions & 0 deletions src/wirecloud/defaulttheme/locale/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ msgstr "Contraseña"
msgid "Sign in"
msgstr "Iniciar sessión"

#: templates/registration/login.html:61
msgid "Sign In with Verifiable Credentials"
msgstr "Iniciar sesión con Credenciales Verificables"

#: templates/registration/login.html:67
msgid "Invalid Verifiable Credential"
msgstr "Credenciales Verificables invalidas"

#: templates/registration/login.html:69
msgid "Unable to login using Verifiable Credentials"
msgstr "No se pudo loguear usando Credenciales Verificables"

#: templates/registration/login.html:71
msgid "An unexpected error has occurred. Please try again later"
msgstr "Error inexperado. Por favor, intentelo más tarde"

#: templates/wirecloud/catalogue/main_resource_details.html:3
msgid "Description"
msgstr "Descripción"
Expand Down Expand Up @@ -389,3 +405,11 @@ msgstr ""
" <li>Instalar otra versión del widget y usar la opción "
"<em>Actualizar/Desactualizar</em></li>\n"
" </ul>"

#: 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"
2 changes: 1 addition & 1 deletion src/wirecloud/defaulttheme/static/css/wirecloud_core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@ body {
height: 20px;
background: url("../images/logos/wc1-mini.png");
background-size: 33px 20px;
}
}
100 changes: 64 additions & 36 deletions src/wirecloud/defaulttheme/templates/registration/login.html
Original file line number Diff line number Diff line change
@@ -1,66 +1,94 @@
{% load compress i18n wirecloudtags %}{% load static from staticfiles %}
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xml:lang="{{ LANGUAGE_CODE }}"
lang="{{ LANGUAGE_CODE }}">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="{{ LANGUAGE_CODE }}" lang="{{ LANGUAGE_CODE }}">

<head>
<title>{% trans "WireCloud Platform - Sign in" %}</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="{% theme_static "images/favicon.ico" %}" />
<link rel="shortcut icon" type="image/x-icon" href="{% theme_static " images/favicon.ico" %}" />
{% compress css %}
{% platform_css "index" %}
{% endcompress %}
<script type="text/javascript">
var Wirecloud = {};
</script>
<script type="text/javascript" src="{% static "js/wirecloud/BaseRequirements.js" %}"></script>
<script type="text/javascript" src="{% static " js/wirecloud/BaseRequirements.js" %}"></script>
</head>

<body>
<div style="text-align:center;">

{% with wc_logo_id="123456"|make_list|random %}{% with wc_logo="images/logos/wc"|add:wc_logo_id|add:".png" %}
<img style="vertical-align: middle; heigth: 250px; width: 416px;" src="{% theme_static wc_logo %}" />
{% endwith %}{% endwith %}

<div style="display: inline-block; vertical-align:middle; margin: 4ex 10ex; width: 400px;">

<div id="unsupported-browser-msg" class="alert alert-error" style="display: none; text-align: left;">
<h4>{% trans "Your browser seems to lack some required features" %}</h4>
<p>{% blocktrans %}We recommend you to upgrade your browser to the newest version of either <a href="https://www.mozilla.org/firefox">Firefox</a> or <a href="https://www.google.com/chrome">Google Chrome</a> as these are the browsers currently supported by WireCloud.{% endblocktrans %}</p>
</div>
<div id="unsupported-browser-msg" class="alert alert-error" style="display: none; text-align: left;">
<h4>{% trans "Your browser seems to lack some required features" %}</h4>
<p>{% blocktrans %}We recommend you to upgrade your browser to the newest version of either <a
href="https://www.mozilla.org/firefox">Firefox</a> or <a
href="https://www.google.com/chrome">Google Chrome</a> as these are the browsers currently
supported by WireCloud.{% endblocktrans %}</p>
</div>

<form style="text-align: left; width: 400px" id="wc-login-form" method="post">
<div class="panel panel-default">
<form style="text-align: left; width: 400px" id="wc-login-form" method="post">
<div class="panel panel-default">

<div class="panel-heading">
<div class="panel-heading">

{% if form.errors %}
<div class="alert alert-danger">{% blocktrans %}<h4>Your username and password didn't match.</h4><p>Please try again.</p>{% endblocktrans %}</div>
{% endif %}
{% if form.errors %}
<div class="alert alert-danger">{% blocktrans %}<h4>Your username and password didn't match.
</h4>
<p>Please try again.</p>{% endblocktrans %}
</div>
{% endif %}

{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<input type="text" class="se-text-field" style="margin: 3px 0; width: 100%;" name="username" autocapitalize="off" autofocus="autofocus" placeholder="{% trans "Username" %}"/>
<div style="display: table-row;">
<div style="display: table-cell; width: 100%; padding: 3px 3px 3px 0px;"><input type="password" class="se-password-field" style="width: 100%; margin: 0;" name="password" placeholder="{% trans "Password" %}"/></div>
<div style="display: table-cell; padding: 3px 0px 3px 3px;"><input class="se-btn btn-primary" style="margin: 0;" type="submit" value="{% trans 'Sign in' %}" /></div>
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<input type="text" class="se-text-field" style="margin: 3px 0; width: 100%;" name="username"
autocapitalize="off" autofocus="autofocus" placeholder="{% trans 'Username' %}" />
<div style="display: table-row;">
<div style="display: table-cell; width: 100%; padding: 3px 3px 3px 0px;"><input
type="password" class="se-password-field" style="width: 100%; margin: 0;"
name="password" placeholder="{% trans 'Password' %}" /></div>
<div style="display: table-cell; padding: 3px 0px 3px 3px;"><input
class="se-btn btn-primary" style="margin: 0;" type="submit"
value="{% trans 'Sign in' %}" /></div>
</div>
</div>
</div>
</form>
<div class="vc-sso-login">
{% if VC_LOGIN_ENABLED %}
<hr />
<div class="flex items-center justify-center">
<a href="{{ VC_LOGIN_PATH }}" class="se-btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Use your digital wallet to authenticate with SSO' %}">
<i class="fas fa-wallet"></i>
{% trans 'Sign In with Verificable Credentials' %}
</a>
</div>
{% if request.GET.error %}
<div class="alert alert-danger my-3">
{% if request.GET.error == "403001" %}
{% trans 'Invalid Verifiable Credential' %}
{% elif request.GET.error == "403002" %}
{% trans 'Unable to login using Verifiable Credentials' %}
{% else %}
{% trans 'An unexpected error has occurred. Please try again later' %}
{% endif %}
</div>
{% endif %}
{% endif %}
</form>
</div>
</div>
</form>

</div>

</div>

<script type="text/javascript">
try {
Wirecloud.check_basic_requirements();
} catch (e) {
document.getElementById('unsupported-browser-msg').style.display = "";
}
</script>
<script type="text/javascript">
try {
Wirecloud.check_basic_requirements();
} catch (e) {
document.getElementById('unsupported-browser-msg').style.display = "";
}
</script>
</body>
</html>

</html>
Loading