Added OAuth2 Client Credentials Authentication workflow for Mtdc Application + Corrected mistakes on Authorization Code flow
--- 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