From d9ecf80afea53a57ff2ac782cda434591f221bbd Mon Sep 17 00:00:00 2001
From: Stefano Alberto Russo <stefano.russo@gmail.com>
Date: Mon, 11 Jan 2021 21:19:52 +0100
Subject: [PATCH] Added support for OpenID Connect with
 mozilla-django-oidc==1.2.4. Minor refactoring of some user-related parts.

---
 services/webapp/code/rosetta/auth.py          | 26 +++++++++++
 .../webapp/code/rosetta/context_processors.py |  9 ++++
 .../rosetta/core_app/templates/account.html   | 17 ++++++-
 .../core_app/templates/navigation.html        | 18 ++++++++
 .../webapp/code/rosetta/core_app/utils.py     | 28 ++++++++++++
 .../webapp/code/rosetta/core_app/views.py     | 26 +----------
 services/webapp/code/rosetta/settings.py      | 44 +++++++++++++++++++
 services/webapp/code/rosetta/urls.py          |  5 ++-
 services/webapp/requirements.txt              |  1 +
 9 files changed, 147 insertions(+), 27 deletions(-)
 create mode 100644 services/webapp/code/rosetta/auth.py
 create mode 100644 services/webapp/code/rosetta/context_processors.py

diff --git a/services/webapp/code/rosetta/auth.py b/services/webapp/code/rosetta/auth.py
new file mode 100644
index 0000000..90b431b
--- /dev/null
+++ b/services/webapp/code/rosetta/auth.py
@@ -0,0 +1,26 @@
+from mozilla_django_oidc.auth import OIDCAuthenticationBackend
+from .core_app.utils import finalize_user_creation
+
+# Setup logging
+import logging
+logger = logging.getLogger(__name__)
+
+
+class RosettaOIDCAuthenticationBackend(OIDCAuthenticationBackend):
+    
+    def create_user(self, claims):
+        
+        # Call parent user creation function
+        user = super(RosettaOIDCAuthenticationBackend, self).create_user(claims)
+
+        # Add profile, keys etc.
+        finalize_user_creation(user)
+
+        return user
+
+
+    def get_userinfo(self, access_token, id_token, payload):
+
+        # Payload must contain the "email" key
+        return payload
+
diff --git a/services/webapp/code/rosetta/context_processors.py b/services/webapp/code/rosetta/context_processors.py
new file mode 100644
index 0000000..7e6e361
--- /dev/null
+++ b/services/webapp/code/rosetta/context_processors.py
@@ -0,0 +1,9 @@
+import os
+from django.conf import settings
+def export_vars(request):
+    data = {}
+    if settings.OIDC_RP_CLIENT_ID:
+        data['OPENID_ENABLED'] = True
+    else:
+        data['OPENID_ENABLED'] = False        
+    return data
\ No newline at end of file
diff --git a/services/webapp/code/rosetta/core_app/templates/account.html b/services/webapp/code/rosetta/core_app/templates/account.html
index 1e30829..cd36ad5 100644
--- a/services/webapp/code/rosetta/core_app/templates/account.html
+++ b/services/webapp/code/rosetta/core_app/templates/account.html
@@ -22,7 +22,7 @@
         <b>Account ID</b>
         </td>
         <td>
-        {{data.user.username}} | <a href="/logout/">Logout</a>
+        {{data.user.username}}
         </td>
        </tr>
       
@@ -99,9 +99,22 @@
        </tr>
 
       </table>
-
       </form>
       
+      <div style="margin-left:10px; margin-top:40px">
+        {% if OPENID_ENABLED %}
+        <form action="{% url 'oidc_logout' %}" method="post">
+        {% csrf_token %}
+        <input type="submit" value="logout">
+        </form>
+        {% else %}
+        <form action="/logout/" method="get">
+        <input type="submit" value="logout">
+        </form>        
+        {% endif %}
+      </div>
+
+      
       <br/>
       <br/>
       <br/>
diff --git a/services/webapp/code/rosetta/core_app/templates/navigation.html b/services/webapp/code/rosetta/core_app/templates/navigation.html
index 0693114..f8885d2 100644
--- a/services/webapp/code/rosetta/core_app/templates/navigation.html
+++ b/services/webapp/code/rosetta/core_app/templates/navigation.html
@@ -44,6 +44,8 @@
                 <input type="password" class="form-control" placeholder="Password" name='password'>
                 <input type='submit' class="btn btn-lg ha-btn-lg" value='Login' />
                 </form>
+                {% if OPENID %}
+                {% endif %}
               </center>         
             </li>
             <center>
@@ -53,7 +55,23 @@
             </div>
             </center>
             {% endif %}
+
+            {% if OPENID_ENABLED %}
+
+            <li>
+            {% if not user.is_authenticated %}
+                <a href="{% url 'oidc_authentication_init' %}">Login with OpenID Conn. &nbsp;</a>
+            {% endif %}
+            </li>
+            {% endif %}
+
       
         </ul>
+        
+  
+        
+        
+        
+        
     </nav>
     {% endif %}
diff --git a/services/webapp/code/rosetta/core_app/utils.py b/services/webapp/code/rosetta/core_app/utils.py
index 0e3e0d9..20085fe 100644
--- a/services/webapp/code/rosetta/core_app/utils.py
+++ b/services/webapp/code/rosetta/core_app/utils.py
@@ -134,6 +134,34 @@ def random_username():
     return username
 
 
+def finalize_user_creation(user):
+
+    from .models import Profile, KeyPair
+
+    # Create profile
+    logger.debug('Creating user profile for user "{}"'.format(user.email))
+    Profile.objects.create(user=user)
+
+    # Generate user keys
+    out = os_shell('mkdir -p /data/resources/keys/', capture=True)
+    if not out.exit_code == 0:
+        logger.error(out)
+        raise ErrorMessage('Something went wrong in creating user keys folder. Please contact support')
+        
+    command= "/bin/bash -c \"ssh-keygen -q -t rsa -N '' -f /data/resources/keys/{}_id_rsa 2>/dev/null <<< y >/dev/null\"".format(user.username)                        
+    out = os_shell(command, capture=True)
+    if not out.exit_code == 0:
+        logger.error(out)
+        raise ErrorMessage('Something went wrong in creating user keys. Please contact support')
+        
+    
+    # Create key objects
+    KeyPair.objects.create(user = user,
+                          default = True,
+                          private_key_file = '/data/resources/keys/{}_id_rsa'.format(user.username),
+                          public_key_file = '/data/resources/keys/{}_id_rsa.pub'.format(user.username))
+    
+
 def sanitize_shell_encoding(text):
     return text.encode("utf-8", errors="ignore")
 
diff --git a/services/webapp/code/rosetta/core_app/views.py b/services/webapp/code/rosetta/core_app/views.py
index 5d63f68..db6a621 100644
--- a/services/webapp/code/rosetta/core_app/views.py
+++ b/services/webapp/code/rosetta/core_app/views.py
@@ -9,7 +9,7 @@ from django.http import HttpResponse, HttpResponseRedirect
 from django.contrib.auth.models import User
 from django.shortcuts import redirect
 from .models import Profile, LoginToken, Task, TaskStatuses, Container, Computing, KeyPair, ComputingSysConf, ComputingUserConf
-from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_tunnel_host, random_username, setup_tunnel
+from .utils import send_email, format_exception, timezonize, os_shell, booleanize, debug_param, get_tunnel_host, random_username, setup_tunnel, finalize_user_creation
 from .decorators import public_view, private_view
 from .exceptions import ErrorMessage
 
@@ -166,29 +166,7 @@ def register_view(request):
             
             data['user'] = user
 
-            # Create profile
-            logger.debug('Creating user profile for user "{}"'.format(user.email))
-            Profile.objects.create(user=user)
-
-            # Generate user keys
-            out = os_shell('mkdir -p /data/resources/keys/', capture=True)
-            if not out.exit_code == 0:
-                logger.error(out)
-                raise ErrorMessage('Something went wrong in creating user keys folder. Please contact support')
-                
-            command= "/bin/bash -c \"ssh-keygen -q -t rsa -N '' -f /data/resources/keys/{}_id_rsa 2>/dev/null <<< y >/dev/null\"".format(user.username)                        
-            out = os_shell(command, capture=True)
-            if not out.exit_code == 0:
-                logger.error(out)
-                raise ErrorMessage('Something went wrong in creating user keys. Please contact support')
-                
-            
-            # Create key objects
-            KeyPair.objects.create(user = user,
-                                default = True,
-                                private_key_file = '/data/resources/keys/{}_id_rsa'.format(user.username),
-                                public_key_file = '/data/resources/keys/{}_id_rsa.pub'.format(user.username))
-            
+            finalize_user_creation(user)
 
             # Manually set the auth backend for the user
             user.backend = 'django.contrib.auth.backends.ModelBackend'
diff --git a/services/webapp/code/rosetta/settings.py b/services/webapp/code/rosetta/settings.py
index d2e880b..8d89571 100644
--- a/services/webapp/code/rosetta/settings.py
+++ b/services/webapp/code/rosetta/settings.py
@@ -36,6 +36,7 @@ INSTALLED_APPS = [
     'rosetta.core_app',
     'django.contrib.admin',
     'django.contrib.auth',
+    'mozilla_django_oidc',
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.messages',
@@ -67,6 +68,7 @@ TEMPLATES = [
                 'django.template.context_processors.request',
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
+                'rosetta.context_processors.export_vars'
             ],
         },
     },
@@ -225,3 +227,45 @@ LOGGING = {
 
 
 
+#===============================
+#  Auth
+#===============================
+
+OIDC_RP_CLIENT_ID  = os.environ.get('OIDC_RP_CLIENT_ID', None)
+
+if OIDC_RP_CLIENT_ID:
+
+    # Add 'mozilla_django_oidc' authentication backend
+    AUTHENTICATION_BACKENDS = (
+        'django.contrib.auth.backends.ModelBackend',
+        'rosetta.auth.RosettaOIDCAuthenticationBackend'
+    )
+    
+    # Base
+    OIDC_RP_CLIENT_SECRET  = os.environ.get('OIDC_RP_CLIENT_SECRET')
+    OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get('OIDC_OP_AUTHORIZATION_ENDPOINT')
+    OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT')
+    OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT')
+    OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'RS256')
+    OIDC_RP_IDP_SIGN_KEY = os.environ.get('OIDC_RP_IDP_SIGN_KEY', None)
+    OIDC_OP_JWKS_ENDPOINT = os.environ.get('OIDC_OP_JWKS_ENDPOINT', None)
+    
+    # Check
+    if OIDC_RP_SIGN_ALGO == 'RS256':
+        if not OIDC_RP_IDP_SIGN_KEY and not OIDC_OP_JWKS_ENDPOINT:
+            raise ImproperlyConfigured('RS256 OpenID requires OIDC_RP_IDP_SIGN_KEY or OIDC_OP_JWKS_ENDPOINT to be set')
+
+    # Optional
+    OIDC_USE_NONCE =  booleanize(os.environ.get('OIDC_USE_NONCE', False))
+    OIDC_TOKEN_USE_BASIC_AUTH = booleanize(os.environ.get('OIDC_TOKEN_USE_BASIC_AUTH', True))
+    
+    # Non-customizable stuff
+    LOGIN_REDIRECT_URL = '/'
+    LOGOUT_REDIRECT_URL = '/'
+    LOGIN_REDIRECT_URL_FAILURE = '/'
+    
+
+
+
+
+
diff --git a/services/webapp/code/rosetta/urls.py b/services/webapp/code/rosetta/urls.py
index 36b16dc..2a91b45 100644
--- a/services/webapp/code/rosetta/urls.py
+++ b/services/webapp/code/rosetta/urls.py
@@ -63,7 +63,10 @@ urlpatterns = [
 
     # Custom APIs
     path('api/v1/base/agent/', core_app_api.agent_api.as_view(), name='agent_api'),
- 
+
+    # Open ID Connect Auth
+    path('oidc/', include('mozilla_django_oidc.urls')),
+
 ]
 
 # This message here is quite useful when developing in autoreload mode
diff --git a/services/webapp/requirements.txt b/services/webapp/requirements.txt
index 183b2ef..70ef741 100644
--- a/services/webapp/requirements.txt
+++ b/services/webapp/requirements.txt
@@ -5,3 +5,4 @@ djangorestframework==3.9.3
 django-rest-swagger==2.2.0
 dateutils==0.6.6
 sendgrid==5.3.0
+mozilla-django-oidc==1.2.4
-- 
GitLab