Added OAuth2 Client Credentials Authentication workflow for Mtdc Application + Corrected mistakes on Authorization Code flow
authordurandn
Mon, 29 Feb 2016 12:23:37 +0100
changeset 6 39cecdd5260e
parent 5 4407b131a70e
child 7 cb21b50b7793
Added OAuth2 Client Credentials Authentication workflow for Mtdc Application + Corrected mistakes on Authorization Code flow
server/src/README
server/src/README.md
server/src/metaeducation/__init__.py
server/src/metaeducation/auth.py
server/src/metaeducation/mtdc_oauth_provider/provider.py
server/src/metaeducation/mtdc_oauth_provider/views.py
server/src/metaeducation/settings/__init__.py
server/src/metaeducation/settings/dev.py.tmpl
server/src/metaeducation/views.py
server/src/requirement.txt
--- a/server/src/README	Mon Feb 29 12:22:07 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-============
-Metaeducation Platform
-============
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/README.md	Mon Feb 29 12:23:37 2016 +0100
@@ -0,0 +1,22 @@
+# METAEDUCATION DJANGO APP
+
+# INSTALLATION
+
+	$ mkvirtualenv mtdc_renkan_env
+	$ pip install -r requirement.txt
+
+# CONFIGURATION
+
+Configure dev.py.tmpl according to your environment and rename it dev.py
+
+# TEST AND DEV
+
+	$ Python manage.py runserver 0.0.0.0:8001
+	
+It is important to run the server on localhost:8001 if you are going to test with the flask OAuth server in the /oauth/ folder
+
+Then we need to configure the Django-Allauth Social App. 
+You will need a Client ID and Client Secret registered by the OAuth server that you will interact with.
+
+For this you need to log into the admin panel (after creating an admin superuser), and add an entry in the "Social Application" table. 
+You must assign a site to Renkan Social Application, which will be the base URL of Renkan (on test and dev, 127.0.0.1:8001)
\ No newline at end of file
--- a/server/src/metaeducation/__init__.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/__init__.py	Mon Feb 29 12:23:37 2016 +0100
@@ -1,3 +1,5 @@
+import signals
+
 VERSION = (0, 0, 1, "alpha", 0)
 
 VERSION_STR = ".".join(map(lambda i:"%02d" % (i,), VERSION[:2]))
@@ -45,3 +47,4 @@
 
 
 __version__ = get_version(VERSION)
+
--- a/server/src/metaeducation/auth.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/auth.py	Mon Feb 29 12:23:37 2016 +0100
@@ -1,12 +1,33 @@
 from rest_framework.authentication import BaseAuthentication
+from django.contrib.auth import get_user_model, login
+from django.contrib.auth.models import Permission
+from django.conf import settings
+import requests
+import re
+import json
 
-class OAuth2ClientCredentialsAuthentication(BaseAuthentication):
+class MtdcOAuth2ClientCredentialsAuthentication(BaseAuthentication):
     
     def authenticate(self, request):
         # get token, get username
+        if 'act_as' not in request.GET or 'HTTP_RENKAN_ACT_AS' not in request.META:
+            return
+        else:
+            username = request.GET.get('act_as', request.META.get("HTTP_RENKAN_ACT_AS", ""))
+        try: 
+            user = get_user_model().objects.get(username=username)
+        except get_user_model().DoesNotExist:
+            return
+        if 'HTTP_AUTHORIZATION' not in request.META:
+            return
+        else:
+            token = re.search("(?<=\s).*", request.META["HTTP_AUTHORIZATION"]).group(0)
         
         # send token to Oauth server
-        
-        # if token is authorized, login user
+        token_validate_response = requests.get(
+            settings.MTDC_VALIDATE_TOKEN_URL+token
+        )
+        if token_validate_response.status_code != 200:
+            return
         return (user, None)
     
--- a/server/src/metaeducation/mtdc_oauth_provider/provider.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/mtdc_oauth_provider/provider.py	Mon Feb 29 12:23:37 2016 +0100
@@ -10,17 +10,13 @@
     package = 'metaeducation.mtdc_oauth_provider'
     
     def extract_uid(self, data):
+        print("retrieved data: "+str(data))
         return data.get('id', '')
 
     def extract_common_fields(self, data):
-        print(data)
         return {"username": data.get("username", "")}
 
     def extract_extra_data(self, data):
-        print(data)
-        return {}
-    
-    def get_default_scope(self):
-        return ['basic']
+        return {"username": data.get("username", "")}
     
 providers.registry.register(MtdcProvider)
\ No newline at end of file
--- a/server/src/metaeducation/mtdc_oauth_provider/views.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/mtdc_oauth_provider/views.py	Mon Feb 29 12:23:37 2016 +0100
@@ -4,6 +4,8 @@
 
 from django.core.exceptions import PermissionDenied
 from django.core.urlresolvers import reverse
+from django.contrib.auth.models import Permission
+from django.contrib.auth import get_user_model
 from django.http import HttpResponseRedirect
 from django.utils import timezone
 
@@ -16,50 +18,78 @@
 
 from allauth.socialaccount.helpers import complete_social_login, render_authentication_error
 from allauth.socialaccount.models import SocialToken, SocialLogin
-from allauth.utils import get_request_param
+from allauth.account import app_settings
+from allauth.account.utils import perform_login
+from allauth.utils import build_absolute_uri, get_request_param
 from allauth.socialaccount.providers.base import AuthAction, AuthError
+from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
 from django.conf import settings
 from urllib.parse import urlparse
 
 from .provider import MtdcProvider
 
 
-class MtdcOAuth2Adapter(OAuth2Adapter):
+class MtdcOAuth2Adapter(OAuth2Adapter, DefaultSocialAccountAdapter):
     provider_id = MtdcProvider.id
     supports_state = False
     
+    oauth_base_url = ""
     access_token_url = ""
     authorize_url = ""
     profile_url = ""
     
-    def __init__(self, request):
-        oauth_base_url = request.GET.get("context", None)
-        if oauth_base_url is None:
-            parsed_referer = urlparse(request.META.get("HTTP_REFERER",""))
-            oauth_base_url = '{uri.scheme}://{uri.netloc}'.format(uri=parsed_referer)
-            print(oauth_base_url)
-        self.access_token_url = oauth_base_url + settings.MTDC_ACCESS_TOKEN_URL
-        self.authorize_url = oauth_base_url + settings.MTDC_AUTHORIZE_URL
-        self.profile_url = oauth_base_url + settings.MTDC_PROFILE_URL
-        print(oauth_base_url)
+    def __init__(self, request=None):
+        if request:
+            if request.session.get("OAUTH_CONTEXT_BASE_URL", None) is None:
+                request.session["OAUTH_CONTEXT_BASE_URL"] = request.GET.get("context", None)
+            self.oauth_base_url = request.session.get("OAUTH_CONTEXT_BASE_URL", None)
+            self.access_token_url = self.oauth_base_url + settings.MTDC_ACCESS_TOKEN_URL
+            self.authorize_url = self.oauth_base_url + settings.MTDC_AUTHORIZE_URL
+            self.profile_url = self.oauth_base_url + settings.MTDC_PROFILE_URL
     
-    #def pre_social_login(self, request, sociallogin):
-    #    base_url = request.GET.get("context")
-    #    print(base_url)
-    #    self.oauth_base_url = base_url
-    #    self.access_token_url = self.oauth_base_url + settings.ITOP_ACCESS_TOKEN_URL
-    #    self.authorize_url = self.oauth_base_url + settings.ITOP_AUTHORIZE_URL
-    #    self.profile_url = self.oauth_base_url + settings.ITOP_PROFILE_URL
+    def pre_social_login(self, request, sociallogin):
+        user = sociallogin.user   
+        try:
+            user = get_user_model().objects.get(username=user.username)  # if user exists, connect the account to the existing account and login
+            sociallogin.state['process'] = 'connect'                
+            perform_login(request, user, 'none')
+        except get_user_model().DoesNotExist:
+            pass
     
     def get_login_redirect_url(self, request):
-        print("GET_LOGIN_REDIRECT_URL?")
         return super(MtdcOAuth2Adapter, self).get_login_redirect_url(self, request)
-
+    
+    def new_user(self, request, sociallogin):
+        if 'username' in sociallogin.account.extra_data:
+            user_queryset = get_user_model().objects.filter(username=sociallogin.account.extra_data['username'])
+            if user_queryset.exists():
+                user = user_queryset.first()
+            else:
+                user = get_user_model()()
+                user.username = sociallogin.account.extra_data.get('username', '')
+            return user
+        else:
+            return get_user_model()()
+        
+    def populate_user(self,
+                      request,
+                      sociallogin,
+                      data):
+        username = data.get('username')
+        user = sociallogin.user
+        user.username = username
+        user.save()
+        add_permission = Permission.objects.get(codename="add_renkan")
+        user.user_permissions.add(add_permission)
+        return user
+    
     def complete_login(self, request, app, token, **kwargs):
-        print("COMPLETE_LOGIN")
         resp = requests.get(self.profile_url,
                             params={'access_token': token.token})
+        print(resp.text)
         extra_data = resp.json()
+        if request.session.get("OAUTH_CONTEXT_BASE_URL", None) is not None:
+            del request.session["OAUTH_CONTEXT_BASE_URL"]
         return self.get_provider().sociallogin_from_response(request,
                                                              extra_data)
 
@@ -73,63 +103,13 @@
             return self.dispatch(request, *args, **kwargs)
         return view
 
-
-class MtdcOAuth2LoginView(MtdcOAuth2View):
-    def dispatch(self, request):
-        provider = self.adapter.get_provider()
-        app = provider.get_app(self.request)
-        client = self.get_client(request, app)
-        action = request.GET.get('action', AuthAction.AUTHENTICATE)
-        auth_url = self.adapter.authorize_url
-        auth_params = provider.get_auth_params(request, action)
-        client.state = SocialLogin.stash_state(request)
-        try:
-            return HttpResponseRedirect(client.get_redirect_url(
-                auth_url, auth_params))
-        except OAuth2Error as e:
-            return render_authentication_error(
-                request,
-                provider.id,
-                exception=e)
-
-
-class MtdcOAuth2CallbackView(MtdcOAuth2View):
+class MtdcOAuth2LoginView(MtdcOAuth2View, OAuth2LoginView):
     def dispatch(self, request):
-        if 'error' in request.GET or 'code' not in request.GET:
-            # Distinguish cancel from error
-            auth_error = request.GET.get('error', None)
-            if auth_error == self.adapter.login_cancelled_error:
-                error = AuthError.CANCELLED
-            else:
-                error = AuthError.UNKNOWN
-            return render_authentication_error(
-                request,
-                self.adapter.provider_id,
-                error=error)
-        app = self.adapter.get_provider().get_app(self.request)
-        client = self.get_client(request, app)
-        try:
-            access_token = client.get_access_token(request.GET['code'])
-            token = self.adapter.parse_token(access_token)
-            token.app = app
-            login = self.adapter.complete_login(request,
-                                                app,
-                                                token,
-                                                response=access_token)
-            login.token = token
-            if self.adapter.supports_state:
-                login.state = SocialLogin \
-                    .verify_and_unstash_state(
-                        request,
-                        get_request_param(request, 'state'))
-            else:
-                login.state = SocialLogin.unstash_state(request)
-            return complete_social_login(request, login)
-        except (PermissionDenied, OAuth2Error) as e:
-            return render_authentication_error(
-                request,
-                self.adapter.provider_id,
-                exception=e)
+        return super(MtdcOAuth2LoginView, self).dispatch(request)
+
+class MtdcOAuth2CallbackView(MtdcOAuth2View, OAuth2CallbackView):
+    def dispatch(self, request):
+        return super(MtdcOAuth2CallbackView, self).dispatch(request)
 
 
 oauth2_login = MtdcOAuth2LoginView.adapter_view(MtdcOAuth2Adapter)
--- a/server/src/metaeducation/settings/__init__.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/settings/__init__.py	Mon Feb 29 12:23:37 2016 +0100
@@ -46,6 +46,14 @@
     'allauth.account.auth_backends.AuthenticationBackend',
 )
 
+REST_FRAMEWORK = {
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'metaeducation.auth.MtdcOAuth2ClientCredentialsAuthentication',
+        'rest_framework.authentication.BasicAuthentication',
+        'rest_framework.authentication.SessionAuthentication',
+    )
+}
+
 MIDDLEWARE_CLASSES = (
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.common.CommonMiddleware',
@@ -86,6 +94,8 @@
 
 USE_TZ = True
 
+SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
+
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/1.8/howto/static-files/
 STATIC_URL = '/static/'
@@ -109,6 +119,7 @@
 
 #ACCOUNT_AUTHENTICATION_METHOD = "username"
 #ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter"
+SOCIALACCOUNT_ADAPTER = "metaeducation.mtdc_oauth_provider.views.MtdcOAuth2Adapter"
 SOCIALACCOUNT_PROVIDERS = {
     'mtdc': {
         'SCOPE': ['basic']
--- a/server/src/metaeducation/settings/dev.py.tmpl	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/settings/dev.py.tmpl	Mon Feb 29 12:23:37 2016 +0100
@@ -39,6 +39,8 @@
 
 DEFAULT_RENKAN_ICON = ""
 
-ITOP_ACCESS_TOKEN_URL = ""
-ITOP_AUTHORIZE_URL = ""
-ITOP_PROFILE_URL = ""
\ No newline at end of file
+MTDC_ACCESS_TOKEN_URL = "" # This URL is the access token endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
+MTDC_AUTHORIZE_URL = "" # This URL is the authorize endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
+MTDC_PROFILE_URL = "" # This URL is the user profile endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
+
+MTDC_VALIDATE_TOKEN_URL = "" # This URL is the ABSOLUTE url for validating a token. There will be no context involved for validation token from server to server.
\ No newline at end of file
--- a/server/src/metaeducation/views.py	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/metaeducation/views.py	Mon Feb 29 12:23:37 2016 +0100
@@ -1,4 +1,3 @@
-from django.contrib.auth import authenticate, login, logout
 from django.core.urlresolvers import reverse
 from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render, get_object_or_404
--- a/server/src/requirement.txt	Mon Feb 29 12:22:07 2016 +0100
+++ b/server/src/requirement.txt	Mon Feb 29 12:23:37 2016 +0100
@@ -1,1 +1,13 @@
-django==1.9.1
+defusedxml==0.4.1
+Django==1.9.1
+django-allauth==0.24.1
+django-guardian==1.4.1
+djangorestframework==3.3.2
+easy-thumbnails==2.3
+oauthlib==1.0.3
+Pillow==3.1.0
+psycopg2==2.6.1
+python3-openid==3.0.9
+requests==2.9.1
+requests-oauthlib==0.6.0
+six==1.10.0