From 252fa668a314736a8ced8ad7eec77164ef7f3c1f Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Wed, 31 May 2017 11:41:11 -0400 Subject: [PATCH] fixes bug 1367513 - add mozilla-django-oidc (#127) * fixes bug 1367513 - add mozilla-django-oidc * python-jose wheel sha * fake OIDC_RP_CLIENT_ID in tests --- .env-dist | 3 +++ docs/configuration.rst | 35 +++++++++++++++++++++++++++++++++++ requirements.txt | 11 +++++++++++ tecken/settings.py | 33 +++++++++++++++++++++++++++++++-- tecken/urls.py | 8 ++++++-- tecken/views.py | 21 ++++++++++++++++++--- tests/test_dashboard.py | 32 +++++++++++++++++++++++++++----- 7 files changed, 131 insertions(+), 12 deletions(-) diff --git a/.env-dist b/.env-dist index 2eca5251b..d1c8db3dd 100644 --- a/.env-dist +++ b/.env-dist @@ -17,3 +17,6 @@ AWS_DEFAULT_REGION=us-west-2 #DJANGO_STATSD_HOST=localhost #DJANGO_STATSD_PORT=8125 #DJANGO_STATSD_NAMESPACE= + +#DJANGO_OIDC_RP_CLIENT_ID= +#DJANGO_OIDC_RP_CLIENT_SECRET= diff --git a/docs/configuration.rst b/docs/configuration.rst index ce4e1d0f7..367be0ac5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -143,3 +143,38 @@ The three environment variables to control the statsd are as follows 2. ``DJANGO_STATSD_PORT`` (*8125*) 3. ``DJANGO_STATSD_NAMESPACE`` (*''* (empty string)) + + +Auth0 +===== + +For authentication to work, you need to have an Auth0 account and its +credentials. You also need a domain so you can figure out certain +URLs. You need the client ID and the client secret. Put these into +the environment variables like this: + +.. code-block:: shell + + DJANGO_OIDC_RP_CLIENT_ID=clientidhereclientidhere + DJANGO_OIDC_RP_CLIENT_SECRET=clientsecrethereclientsecrethere + +The default domain is ``auth.mozilla.auth0.com``. That has consequently +been used to set up the following defaults: + +.. code-block:: shell + + DJANGO_OIDC_OP_AUTHORIZATION_ENDPOINT=https://auth.mozilla.auth0.com/authorize + DJANGO_OIDC_OP_TOKEN_ENDPOINT=https://auth.mozilla.auth0.com/oauth/token + DJANGO_OIDC_OP_USER_ENDPOINT=https://auth.mozilla.auth0.com/userinfo + +If your domain is different, override these above three environment +variables with your domain. + +Note! Tecken uses `Auth0`_ which follows the OpenID Connect protocol. +The configuration actually requires the above mentioned URLs and when +you use Auth0, the URLs are quite constant. But if you use another OpenID +Connect provider, use the domain (e.g. ``myoidc.example.com``) and go to +``https://myoidc.example.com/.well-known/openid-configuration`` and from +there it should publish the authorization, token and user endpoints. + +.. _`Auth0`: https://auth0.com/ diff --git a/requirements.txt b/requirements.txt index 244cd0540..1ef5937f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -275,6 +275,17 @@ vine==1.1.3 \ django_celery_results==1.0.1 \ --hash=sha256:dfa240fb535a1a2d01c9e605ad71629909318eae6b893c5009eafd7265fde10b \ --hash=sha256:8bca2605eeff4418be7ce428a6958d64bee0f5bdf1f8e563fbc09a9e2f3d990f +mozilla-django-oidc==0.1.10 \ + --hash=sha256:620029825c6fb66da26a79426595ee8d863c0610ca706dc0c83bbcd8cde25242 \ + --hash=sha256:f674ad617c0a0d73c9ea8f85bfa6067bd45fbd802c8437ad8faa21899b41cb3b +python-jose==1.3.2 \ + --hash=sha256:35eca894ab91db1774251296949679396c46d6bc506ea03804b8f7a7d0204392 \ + --hash=sha256:968254f57ccd0fc99ab9557f82b90a5b23ccaf3716e8817d8edaa9f21c21bb2d +pycrypto==2.6.1 \ + --hash=sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c +ecdsa==0.13 \ + --hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \ + --hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa # diff --git a/tecken/settings.py b/tecken/settings.py index 17790c0b3..51f632141 100644 --- a/tecken/settings.py +++ b/tecken/settings.py @@ -118,6 +118,9 @@ class Core(CSP, AWS, Configuration, Celery): 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + # Third party apps, that need to be listed last + 'mozilla_django_oidc', ] MIDDLEWARE_CLASSES = ( @@ -131,6 +134,7 @@ class Core(CSP, AWS, Configuration, Celery): 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'csp.middleware.CSPMiddleware', + 'mozilla_django_oidc.contrib.auth0.middleware.RefreshIDToken', ) ROOT_URLCONF = 'tecken.urls' @@ -140,8 +144,7 @@ class Core(CSP, AWS, Configuration, Celery): # Add the django-allauth authentication backend. AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', - # 'allauth.account.auth_backends.AuthenticationBackend', - # 'guardian.backends.ObjectPermissionBackend', + 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', ) MESSAGE_TAGS = { @@ -194,6 +197,25 @@ class Core(CSP, AWS, Configuration, Celery): }, ] + OIDC_RP_CLIENT_ID = values.SecretValue() + OIDC_RP_CLIENT_SECRET = values.SecretValue() + + OIDC_OP_AUTHORIZATION_ENDPOINT = values.URLValue( + 'https://auth.mozilla.auth0.com/authorize' + ) + OIDC_OP_TOKEN_ENDPOINT = values.URLValue( + 'https://auth.mozilla.auth0.com/oauth/token' + ) + OIDC_OP_USER_ENDPOINT = values.URLValue( + 'https://auth.mozilla.auth0.com/userinfo' + ) + + # Let cookies last quite a long time. + SESSION_COOKIE_AGE = values.IntegerValue(60 * 60 * 24 * 100) + + # Where users get redirected after successfully signing in + LOGIN_REDIRECT_URL = '/?signedin=true' + class Base(Core): """Settings that may change per-environment, some with defaults.""" @@ -401,6 +423,9 @@ class Test(Dev): SECRET_KEY = values.Value('not-so-secret-after-all') + OIDC_RP_CLIENT_ID = values.Value('not-so-secret-after-all') + OIDC_RP_CLIENT_SECRET = values.Value('not-so-secret-after-all') + PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.MD5PasswordHasher', ) @@ -410,6 +435,10 @@ class Test(Dev): 'https://s3.example.com/private/prefix/', ]) + AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + ) + class Stage(Base): """Configuration to be used in stage environment""" diff --git a/tecken/urls.py b/tecken/urls.py index 7fb579895..9dadbd8df 100644 --- a/tecken/urls.py +++ b/tecken/urls.py @@ -16,13 +16,17 @@ urlpatterns = [ url(r'^$', views.dashboard, name='dashboard'), url(r'^__task_tester__$', views.task_tester, name='task_tester'), - url(r'', include('tecken.download.urls', namespace='download')), - + url( + r'', + include('tecken.download.urls', namespace='download') + ), url( r'symbolicate/', include('tecken.symbolicate.urls', namespace='symbolicate') ), + url(r'^oidc/', include('mozilla_django_oidc.urls')), + url( r'^(?Pcontribute\.json)$', static.serve, diff --git a/tecken/views.py b/tecken/views.py index 786bf848b..e1feeda51 100644 --- a/tecken/views.py +++ b/tecken/views.py @@ -6,9 +6,9 @@ from django import http from django.template import TemplateDoesNotExist, loader -from django.template.response import TemplateResponse from django.views.decorators.csrf import csrf_exempt from django.core.cache import cache +from django.core.urlresolvers import reverse from .symbolicate.views import symbolicate_json from tecken.tasks import sample_task @@ -23,8 +23,23 @@ def dashboard(request): if request.method == 'POST' and request.body: return symbolicate_json(request) - context = {} - return TemplateResponse(request, 'tecken/dashboard.html', context=context) + user = {} + if request.user.is_authenticated: + user['email'] = request.user.email + user['active'] = request.user.is_active + user['sign_out_url'] = request.build_absolute_uri( + reverse('oidc_logout') + ) + else: + user['sign_in_url'] = request.build_absolute_uri( + reverse('oidc_authentication_init') + ) + + context = { + 'user': user, + 'documentation': 'https://tecken.readthedocs.io', + } + return http.JsonResponse(context) def server_error(request, template_name='500.html'): diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 5972aadc8..50e264094 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -2,12 +2,10 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, you can obtain one at http://mozilla.org/MPL/2.0/. -# import pytest -# from django.core.urlresolvers import reverse -# from django.utils import timezone +import pytest + +from django.contrib.auth.models import User -# from atmo.clusters.models import Cluster -# from atmo.jobs.models import SparkJob from tecken.views import server_error @@ -18,3 +16,27 @@ def test_server_error(rf): response = server_error(request, template_name='non-existing.html') assert response.status_code == 500 + + +@pytest.mark.django_db +def test_dashboard(client): + response = client.get('/') + assert response.status_code == 200 + information = response.json() + assert 'documentation' in information + assert 'sign_in_url' in information['user'] + + # Now pretend the user goes through the OIDC steps to sign in + user = User.objects.create(username='peterbe', email='peterbe@example.com') + user.set_password('secret') + user.save() + assert client.login(username='peterbe', password='secret') + + response = client.get('/') + assert response.status_code == 200 + information = response.json() + assert 'documentation' in information + assert 'sign_in_url' not in information['user'] + assert 'sign_out_url' in information['user'] + assert information['user']['email'] == 'peterbe@example.com' + assert information['user']['active']