Skip to content

Commit

Permalink
[UCDC]borovenskiy/feature/sso (#19)
Browse files Browse the repository at this point in the history
Youtrack task - https://youtrack.raccoongang.com/issue/UCDC-49.

PR contains changes for integrating Single Sign On for UCDC portal and Edx platform. 
UCDC portal is developed using the Django framework.

**Doc to installation**
https://docs.google.com/document/d/1TpgeIMeFCRRofSO0H082Rm0uSNH4IDBDVFzrYBg6GMQ/edit?usp=sharing

**IMPORTANT**: After a merge of the PR please create a Tag. ucdc-v.0.1.0
  • Loading branch information
NikolayBorovenskiy authored and Igor Degtiarov committed Sep 17, 2019
1 parent 54395fe commit cfe431e
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 93 deletions.
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Redirect uri must be **http://<edx_url>/auth/complete/custom-oauth2/**

- Install this client
```
pip install git+https://github.com/raccoongang/edx-oauth-client.git@hawthorn-master#egg=edx_oauth_client
pip install git+https://github.com/raccoongang/edx-oauth-client.git@ucdc-v.0.1.0#egg=edx_oauth_client==ucdc-v.0.1.0
```

- Enable THIRD_PARTY_AUTH in edX
Expand All @@ -23,15 +23,17 @@ Redirect uri must be **http://<edx_url>/auth/complete/custom-oauth2/**
...
"CUSTOM_OAUTH_PARAMS": {
"PROVIDER_URL": "https://example.com",
"AUTHORIZE_URL": "/oauth2/authorize",
"GET_TOKEN_URL": "/oauth2/access_token",
"PROVIDER_ID_KEY": "<unique identifier>",
"PROVIDER_NAME": "custom-oauth2",
"USER_DATA_URL": "/api/v0/users/me",
"COOKIE_NAME": "cookie_name", # If you're want seamless authorization
"COURSES_LIST_URL_PATH": "courses", # write if course_list redirection is needed
"USER_ACCOUNT_URL_PATH": "account", # write if user account redirection is needed
"DASHBOARD_URL_PATH": "user" # write if dashboard redirection is needed
"AUTHORIZE_URL": "/o/authorize",
"GET_TOKEN_URL": "/o/token/",
"PROVIDER_ID_KEY": "<unique identifier>", # This should be attribute name. For example, `email`, `uid`, etc. Depends on that attributes which OAuth provider is able to handle and return to Edx as json payload data. Be aware: this is not provider's secret key.
"PROVIDER_NAME": "ucdc_oauth2",
"USER_DATA_URL": "/user/current/",
"COOKIE_NAME": "cookie_name", # If you're want seamless authorization. Suggested name is `authenticated`.
"COOKIE_DOMAIN": "domain name", # Common domain name for portal and Edx platform. For example, we have two domains `ucdc.devstack.lms` and `edx.devstack.lms`. The common damain name for both is `devstack.lms`.
"COURSES_LIST_URL_PATH": "courses", # write if course_list redirection is needed. From edx course list to ucdc portal course list. Leave it as blank if you want to avoid the redirection.
"USER_ACCOUNT_URL_PATH": "account", # write if user account redirection is needed. From edx account page to ucdc portal account page. Leave it as blank if you want to avoid the redirection.
"DASHBOARD_URL_PATH": "dashboard" # write if dashboard redirection is needed. From edx dasboard page to ucdc portal dasboard page. Leave it as blank if you want to avoid the redirection.
"LOGOUT_URL_PATH": "logout" # ucdc portal logout url. By default `logout`. This is necessary so that when logout of the Edx there is a transition to the logout of the portal.
},
"THIRD_PARTY_AUTH_BACKENDS":["edx_oauth_client.backends.generic_oauth_client.GenericOAuthBackend"],
Expand All @@ -45,13 +47,13 @@ Redirect uri must be **http://<edx_url>/auth/complete/custom-oauth2/**

- Add provider config in edX admin panel /admin/third_party_auth/oauth2providerconfig/
- Enabled - **true**
- backend-name - **custom-oauth2**
- backend-name - **ucdc_oauth2**
- Skip registration form - **true**
- Skip email verification - **true**
- Visible - **true**
- Client ID from Provider Admin OAuth Tab
- Client Secret from Provider Admin OAuth Tab
- Make it visible ? + link on Edx
- name slug should be the same as provider name ? temp
- name - **ucdc_oauth2**

- If you're want seamless authorization add middleware classes for
SeamlessAuthorization (crossdomain cookie support needed).
Expand Down
8 changes: 7 additions & 1 deletion edx_oauth_client/backends/generic_oauth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
]


class UserIdKeyIsMissed(Exception):
pass


class GenericOAuthBackend(BaseOAuth2):
"""
Backend for Generic OAuth Server Authorization.
Expand Down Expand Up @@ -124,5 +128,7 @@ def get_user_id(self, details, response):
id_key = response.get(self.ID_KEY)

if not id_key:
log.error("ID_KEY is not found in the User data response. SSO won't work correctly")
msg = "ID_KEY is not found in the User data response. SSO won't work correctly. ID_KEY: %s" % self.ID_KEY
raise UserIdKeyIsMissed(msg)

return id_key
43 changes: 43 additions & 0 deletions edx_oauth_client/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Constants for the operation of the module.
Keep immutable values here so as not to clog the namespace of OAuth backend and middleware layer.
"""


OAUTH_PROCESS_URLS = ("oauth2", "auth", "login_oauth_token", "social-logout")
API_URLS = (
"certificates",
"api",
"user_api",
"notifier_api",
"update_example_certificate",
"update_certificate",
"request_certificate",
)

LOCAL_URLS = (
"i18n",
"search",
"verify_student",
"certificates",
"jsi18n",
"course_modes",
"404",
"500",
"i18n.js",
"wiki",
"notify",
"courses",
"xblock",
"change_setting",
"account",
"notification_prefs",
"admin",
"survey",
"event",
"instructor_task_status",
"edinsights_service",
"openassessment",
"instructor_report",
"logout",
)
137 changes: 63 additions & 74 deletions edx_oauth_client/middleware.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import logging
import re
import os.path

from urlparse import urljoin, urlparse

from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.contrib.auth import logout, REDIRECT_FIELD_NAME
from django.shortcuts import redirect
from django.urls import reverse
from social_django.views import auth, NAMESPACE

from edx_oauth_client.backends.generic_oauth_client import GenericOAuthBackend
from edx_oauth_client.constants import API_URLS, LOCAL_URLS, OAUTH_PROCESS_URLS

from social_django.views import auth, NAMESPACE
log = logging.getLogger(__name__)

try:
from opaque_keys.edx.keys import CourseKey
except ImportError:
msg = "Oh, it's not edx"
pass
log.warn("Oh, it's not edx")


class SeamlessAuthorization(object):
cookie_name = GenericOAuthBackend.CUSTOM_OAUTH_PARAMS.get("COOKIE_NAME", "authenticated")
cookie_domain = GenericOAuthBackend.CUSTOM_OAUTH_PARAMS.get("COOKIE_DOMAIN")
provider_url = GenericOAuthBackend.CUSTOM_OAUTH_PARAMS.get("PROVIDER_URL", "")
provider_logout_url_path = GenericOAuthBackend.CUSTOM_OAUTH_PARAMS.get("LOGOUT_URL_PATH", "logout")

cookie_name = GenericOAuthBackend.CUSTOM_OAUTH_PARAMS.get('COOKIE_NAME', 'authenticated')
def process_response(self, request, response):
"""
Delete cross-domain cookie of SSO flow to accomplish logout.
"""
if not request.session.get(self.cookie_name) and self.cookie_domain:
response.set_cookie(
self.cookie_name,
domain=self.cookie_domain,
max_age=0,
expires="Thu, 01-Jan-1970 00:00:00 GMT",
)

return response

def process_request(self, request):
"""
Expand All @@ -29,15 +49,14 @@ def process_request(self, request):
current_url = request.get_full_path()

# SeamlessAuthorization doesn't work for Django administration
if hasattr(settings, 'SOCIAL_AUTH_EXCLUDE_URL_PATTERN'):
if hasattr(settings, "SOCIAL_AUTH_EXCLUDE_URL_PATTERN"):
r = re.compile(settings.SOCIAL_AUTH_EXCLUDE_URL_PATTERN)
if r.match(current_url):
return None

auth_cookie = request.COOKIES.get(self.cookie_name)
auth_cookie_portal = request.session.get(self.cookie_name)
continue_url = reverse('{0}:complete'.format(NAMESPACE),
args=(backend,))
continue_url = reverse("{0}:complete".format(NAMESPACE), args=(backend,))
is_auth = request.user.is_authenticated()
is_same_user = (auth_cookie == auth_cookie_portal)

Expand All @@ -46,14 +65,22 @@ def process_request(self, request):

request.session[self.cookie_name] = auth_cookie

if reverse("logout") == current_url:
del request.session[self.cookie_name]
logout(request)
# FIXME: redirect is dirty fix to be able logout external portal.
# Please try to avoid and don't do it at edx side.
return redirect(urljoin(self.provider_url, self.provider_logout_url_path))

if not is_same_user and is_auth:
logout(request)

if (auth_cookie and not is_continue and (not is_auth or not is_same_user)) or \
('force_auth' in request.session and request.session.pop('force_auth')):
if (auth_cookie and not is_continue and (not is_auth or not is_same_user)) or (
"force_auth" in request.session and request.session.pop("force_auth")
):
query_dict = request.GET.copy()
query_dict[REDIRECT_FIELD_NAME] = current_url
query_dict['auth_entry'] = 'login'
query_dict["auth_entry"] = "login"
request.GET = query_dict
logout(request)
return auth(request, backend)
Expand All @@ -69,69 +96,31 @@ def process_request(self, request):
"""
Redirect to PLP for pages that have duplicated functionality on PLP.
"""
CUSTOM_OAUTH_PARAMS = settings.CUSTOM_OAUTH_PARAMS if hasattr(settings, 'CUSTOM_OAUTH_PARAMS') else {}
PROVIDER_URL = CUSTOM_OAUTH_PARAMS.get("PROVIDER_URL", "")
CUSTOM_OAUTH_PARAMS = getattr(settings, "CUSTOM_OAUTH_PARAMS", {})

COURSES_LIST_URL_PATH = CUSTOM_OAUTH_PARAMS.get("COURSES_LIST_URL_PATH")
USER_ACCOUNT_URL_PATH = CUSTOM_OAUTH_PARAMS.get("USER_ACCOUNT_URL_PATH")
DASHBOARD_URL_PATH = CUSTOM_OAUTH_PARAMS.get("DASHBOARD_URL_PATH")
provider_url = CUSTOM_OAUTH_PARAMS.get("PROVIDER_URL", "")
dashboard_url_path = CUSTOM_OAUTH_PARAMS.get("DASHBOARD_URL_PATH")
courses_list_url_path = CUSTOM_OAUTH_PARAMS.get("COURSES_LIST_URL_PATH")
user_account_url_path = CUSTOM_OAUTH_PARAMS.get("USER_ACCOUNT_URL_PATH")

current_url = request.get_full_path()
if current_url:
start_url = current_url.split('?')[0].split('/')[1]
else:
start_url = ''

auth_process_urls = ('oauth2', 'auth', 'login_oauth_token', 'social-logout')
api_urls = (
'certificates', 'api', 'user_api', 'notifier_api', 'update_example_certificate', 'update_certificate',
'request_certificate',)

handle_local_urls = (
'i18n', 'search', 'verify_student', 'certificates', 'jsi18n', 'course_modes', '404', '500', 'i18n.js',
'wiki', 'notify', 'courses', 'xblock', 'change_setting', 'account', 'notification_prefs', 'admin',
'survey', 'event', 'instructor_task_status', 'edinsights_service', 'openassessment', 'instructor_report',
'logout'
)

handle_local_urls += auth_process_urls + api_urls
is_auth = request.user.is_authenticated()
start_url_path, _, _ = urlparse(current_url).path.strip("/").partition("/")

available_urls = API_URLS + LOCAL_URLS + OAUTH_PROCESS_URLS
if settings.DEBUG:
debug_handle_local_urls = ('debug', settings.STATIC_URL, settings.MEDIA_URL)
handle_local_urls += debug_handle_local_urls

if request.path in ("/dashboard/", "/dashboard"):
if is_auth and DASHBOARD_URL_PATH:
return redirect(os.path.join(PROVIDER_URL, DASHBOARD_URL_PATH))
else:
return redirect(PROVIDER_URL)

r_url = re.compile(r'^/courses/(.*)/about').match(current_url)
if r_url:
return redirect(
os.path.join(os.path.join(PROVIDER_URL))
)

is_courses_list_or_about_page = False
r = re.compile(r'^/courses/%s/about' % settings.COURSE_ID_PATTERN)

if r.match(current_url):
is_courses_list_or_about_page = True

if COURSES_LIST_URL_PATH and request.path in ("/courses/", "/courses"):
return redirect(os.path.join(PROVIDER_URL, COURSES_LIST_URL_PATH))

if request.path.startswith('/u/') or request.path in ("/account/settings/", "/account/settings"):
if is_auth and USER_ACCOUNT_URL_PATH:
return redirect(os.path.join(PROVIDER_URL, USER_ACCOUNT_URL_PATH))
else:
return redirect(PROVIDER_URL)

if start_url not in handle_local_urls or is_courses_list_or_about_page:
if start_url.split('?')[0] not in handle_local_urls:
provider_url = PROVIDER_URL.rstrip("/") + "/"
return redirect("%s%s" % (provider_url, current_url))

if not is_auth and start_url not in auth_process_urls and start_url not in api_urls:
request.session['force_auth'] = True
debug_handle_local_urls = ("debug", settings.STATIC_URL, settings.MEDIA_URL)
available_urls += debug_handle_local_urls

if request.user.is_authenticated():
if dashboard_url_path and request.path.strip("/") in reverse("dashboard"):
return redirect(urljoin(provider_url, dashboard_url_path))

if courses_list_url_path and request.path.strip("/") in reverse("courses"):
return redirect(urljoin(provider_url, courses_list_url_path))

if user_account_url_path and (
request.path.startswith("/u/") or request.path.strip("/") in reverse("account_settings")
):
return redirect(urljoin(provider_url, user_account_url_path))
elif start_url_path not in (API_URLS + OAUTH_PROCESS_URLS):
request.session["force_auth"] = True
7 changes: 3 additions & 4 deletions edx_oauth_client/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
from logging import getLogger

from student.forms import AccountCreationForm
from django.contrib.auth.models import User
from social_core.pipeline import partial

from openedx.core.djangoapps.user_api.accounts.utils import generate_password
from student.helpers import (
do_create_account,
)
from student.forms import AccountCreationForm
from student.helpers import do_create_account
from third_party_auth.pipeline import AuthEntryError

log = getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name='edx-oauth-client',
version='1.0',
version='ucdc-v.0.1.0',
description='Client OAuth2 from edX installations',
author='edX',
url='https://github.com/raccoongang/edx_oauth_client',
Expand Down

0 comments on commit cfe431e

Please sign in to comment.