diff --git a/services/proxy/default-ssl.conf b/services/proxy/default-ssl.conf index a52f5b0133de50e5fb87f945afb0b6acf2d33b3a..1124d7dc6ce621cce28da113d36f31fb0709ed77 100644 --- a/services/proxy/default-ssl.conf +++ b/services/proxy/default-ssl.conf @@ -133,7 +133,9 @@ downgrade-1.0 force-response-1.0 # MSIE 7 and newer should be able to use keepalive BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown - + + # Required for the Open ID connect redirects to work properly + RequestHeader set X-Forwarded-Proto 'https' env=HTTPS </VirtualHost> </IfModule> diff --git a/services/webapp/code/rosetta/auth.py b/services/webapp/code/rosetta/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..90b431b78bad20ac0bafb13dc5060f6dec02b409 --- /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 0000000000000000000000000000000000000000..7e6e361db2428d42945ac278c2ab319378a45393 --- /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 1e3082927c44b494a134005f4c21314461b0fb85..cd36ad5ee986c06d1ba7d5bd1cedc7b46380a084 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 06931141b962ea877087d70ee979e60d0262305b..f8885d2a80afb0b78849e7ce12a5cce75fd6f237 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. </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 0e3e0d9bacbad410ba1081300c76bb7adf683872..20085fea6f4df9f100054635bd05e34fc3e18b89 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 5d63f68be6f03680866dfdf31a54818ec884a69f..db6a6212063e60800244590523d134b262130b7a 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 d2e880ba6efa644ffda72e042f081a5ef8d21486..6b49a22702d8f98cbf6008b61d16f422a163a52c 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,48 @@ 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 = '/' + #OIDC_AUTHENTICATION_CALLBACK_URL = 'rosetta.local/oidc/callback/' + + # Required for the Open ID connect redirects to work properly + USE_X_FORWARDED_HOST = True + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + + + + diff --git a/services/webapp/code/rosetta/urls.py b/services/webapp/code/rosetta/urls.py index 36b16dc4a9c212cec8a6eeccad02bfaab1e5f86c..2a91b45de51a2365d58be92d9a13fc8b6ebae2be 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 183b2ef62f2bf30c95ef68e92b2fdc0228c56649..70ef741bc80372b48a0fb8c71f5abd00e559f09a 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