# HG changeset patch # User durandn # Date 1456745017 -3600 # Node ID 39cecdd5260e68c898834b026c2049d1ac77fcea # Parent 4407b131a70ebdb26ad6933d171fc1435a65fd0f Added OAuth2 Client Credentials Authentication workflow for Mtdc Application + Corrected mistakes on Authorization Code flow diff -r 4407b131a70e -r 39cecdd5260e server/src/README --- 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 -============ diff -r 4407b131a70e -r 39cecdd5260e server/src/README.md --- /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 diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/__init__.py --- 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) + diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/auth.py --- 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) diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/mtdc_oauth_provider/provider.py --- 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 diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/mtdc_oauth_provider/views.py --- 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) diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/settings/__init__.py --- 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'] diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/settings/dev.py.tmpl --- 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 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 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 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 diff -r 4407b131a70e -r 39cecdd5260e server/src/metaeducation/views.py --- 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 diff -r 4407b131a70e -r 39cecdd5260e server/src/requirement.txt --- 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