diff --git a/requirements.txt b/requirements.txt
index c1df66c43d..9dfd334a16 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,4 @@ pytz==2015.7
django-formtools
qrcode
requests
+requests_oauthlib==0.8.0
diff --git a/seahub/auth/views.py b/seahub/auth/views.py
index e8c2e55ec9..0f7294064a 100644
--- a/seahub/auth/views.py
+++ b/seahub/auth/views.py
@@ -245,6 +245,7 @@ def login(request, template_name='registration/login.html',
enable_shib_login = getattr(settings, 'ENABLE_SHIB_LOGIN', False)
enable_krb5_login = getattr(settings, 'ENABLE_KRB5_LOGIN', False)
enable_adfs_login = getattr(settings, 'ENABLE_ADFS_LOGIN', False)
+ enable_oauth = getattr(settings, 'ENABLE_OAUTH', False)
login_bg_image_path = LOGIN_BG_IMAGE_PATH
# get path that background image of login page
@@ -262,6 +263,7 @@ def login(request, template_name='registration/login.html',
'enable_shib_login': enable_shib_login,
'enable_krb5_login': enable_krb5_login,
'enable_adfs_login': enable_adfs_login,
+ 'enable_oauth': enable_oauth,
'login_bg_image_path': login_bg_image_path,
}, context_instance=RequestContext(request))
diff --git a/seahub/oauth/__init__.py b/seahub/oauth/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/oauth/admin.py b/seahub/oauth/admin.py
new file mode 100644
index 0000000000..8c38f3f3da
--- /dev/null
+++ b/seahub/oauth/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/seahub/oauth/backends.py b/seahub/oauth/backends.py
new file mode 100644
index 0000000000..313523838f
--- /dev/null
+++ b/seahub/oauth/backends.py
@@ -0,0 +1,55 @@
+from django.conf import settings
+from django.contrib.auth.backends import RemoteUserBackend
+
+from seahub.base.accounts import User
+from registration.models import notify_admins_on_activate_request
+
+class OauthRemoteUserBackend(RemoteUserBackend):
+ """
+ This backend is to be used in conjunction with the ``RemoteUserMiddleware``
+ found in the middleware module of this package, and is used when the server
+ is handling authentication outside of Django.
+
+ By default, the ``authenticate`` method creates ``User`` objects for
+ usernames that don't already exist in the database. Subclasses can disable
+ this behavior by setting the ``create_unknown_user`` attribute to
+ ``False``.
+ """
+
+ # Create a User object if not already in the database?
+ create_unknown_user = getattr(settings, 'OAUTH_CREATE_UNKNOWN_USER', True)
+ # Create active user by default.
+ activate_after_creation = getattr(settings, 'OAUTH_ACTIVATE_USER_AFTER_CREATION', True)
+
+ def get_user(self, username):
+ try:
+ user = User.objects.get(email=username)
+ except User.DoesNotExist:
+ user = None
+ return user
+
+ def authenticate(self, remote_user):
+ """
+ The username passed as ``remote_user`` is considered trusted. This
+ method simply returns the ``User`` object with the given username,
+ creating a new ``User`` object if ``create_unknown_user`` is ``True``.
+
+ Returns None if ``create_unknown_user`` is ``False`` and a ``User``
+ object with the given username is not found in the database.
+ """
+ if not remote_user:
+ return
+
+ username = self.clean_username(remote_user)
+ try:
+ user = User.objects.get(email=username)
+ except User.DoesNotExist:
+ if self.create_unknown_user:
+ user = User.objects.create_user(
+ email=username, is_active=self.activate_after_creation)
+ if user and self.activate_after_creation is False:
+ notify_admins_on_activate_request(user.email)
+ else:
+ user = None
+
+ return user
diff --git a/seahub/oauth/migrations/__init__.py b/seahub/oauth/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/oauth/models.py b/seahub/oauth/models.py
new file mode 100644
index 0000000000..71a8362390
--- /dev/null
+++ b/seahub/oauth/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/seahub/oauth/tests.py b/seahub/oauth/tests.py
new file mode 100644
index 0000000000..7ce503c2dd
--- /dev/null
+++ b/seahub/oauth/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/seahub/oauth/urls.py b/seahub/oauth/urls.py
new file mode 100644
index 0000000000..4177b00660
--- /dev/null
+++ b/seahub/oauth/urls.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2012-2016 Seafile Ltd.
+
+from django.conf.urls import patterns, url
+from seahub.oauth.views import oauth_login, oauth_callback
+
+urlpatterns = patterns('',
+ url(r'login/$', oauth_login, name='oauth_login'),
+ url(r'callback/$', oauth_callback, name='oauth_callback'),
+)
diff --git a/seahub/oauth/views.py b/seahub/oauth/views.py
new file mode 100644
index 0000000000..2acc48df53
--- /dev/null
+++ b/seahub/oauth/views.py
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+
+import os
+import logging
+from requests_oauthlib import OAuth2Session
+from django.http import HttpResponseRedirect
+from django.template import RequestContext
+from django.shortcuts import render_to_response
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+
+from seahub import auth
+from seahub.profile.models import Profile
+from seahub.utils import is_valid_email
+import seahub.settings as settings
+
+logger = logging.getLogger(__name__)
+
+ENABLE_OAUTH = getattr(settings, 'ENABLE_OAUTH', False)
+if ENABLE_OAUTH:
+
+ if getattr(settings, 'OAUTH_ENABLE_INSECURE_TRANSPORT', False):
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
+
+ # Used for oauth workflow.
+ CLIENT_ID = getattr(settings, 'OAUTH_CLIENT_ID', '')
+ CLIENT_SECRET = getattr(settings, 'OAUTH_CLIENT_SECRET', '')
+ AUTHORIZATION_URL = getattr(settings, 'OAUTH_AUTHORIZATION_URL', '')
+ REDIRECT_URL = getattr(settings, 'OAUTH_REDIRECT_URL', '')
+ TOKEN_URL = getattr(settings, 'OAUTH_TOKEN_URL', '')
+ USER_INFO_URL = getattr(settings, 'OAUTH_USER_INFO_URL', '')
+ SCOPE = getattr(settings, 'OAUTH_SCOPE', '')
+
+ # Used for init an user for Seahub.
+ PROVIDER_DOMAIN = getattr(settings, 'OAUTH_PROVIDER_DOMAIN', '')
+ ATTRIBUTE_MAP = {
+ 'id': (True, "email"),
+ }
+ ATTRIBUTE_MAP.update(getattr(settings, 'OAUTH_ATTRIBUTE_MAP', {}))
+
+ session = OAuth2Session(client_id=CLIENT_ID,
+ scope=SCOPE, redirect_uri=REDIRECT_URL)
+
+def oauth_check(func):
+ """ Decorator for check if OAuth valid.
+ """
+ def _decorated(request):
+
+ error = False
+ if not ENABLE_OAUTH:
+ logger.error('OAuth not enabled.')
+ error = True
+ else:
+ if not CLIENT_ID or not CLIENT_SECRET or not AUTHORIZATION_URL \
+ or not REDIRECT_URL or not TOKEN_URL or not USER_INFO_URL \
+ or not SCOPE or not PROVIDER_DOMAIN:
+ logger.error('OAuth relevant settings invalid.')
+ logger.error('CLIENT_ID: %s' % CLIENT_ID)
+ logger.error('CLIENT_SECRET: %s' % CLIENT_SECRET)
+ logger.error('AUTHORIZATION_URL: %s' % AUTHORIZATION_URL)
+ logger.error('REDIRECT_URL: %s' % REDIRECT_URL)
+ logger.error('TOKEN_URL: %s' % TOKEN_URL)
+ logger.error('USER_INFO_URL: %s' % USER_INFO_URL)
+ logger.error('SCOPE: %s' % SCOPE)
+ logger.error('PROVIDER_DOMAIN: %s' % PROVIDER_DOMAIN)
+ error = True
+
+ if error:
+ return render_to_response('error.html', {
+ 'error_msg': _('Error, please contact administrator.'),
+ }, context_instance=RequestContext(request))
+
+ return func(request)
+
+ return _decorated
+
+# https://requests-oauthlib.readthedocs.io/en/latest/examples/github.html
+# https://requests-oauthlib.readthedocs.io/en/latest/examples/google.html
+@oauth_check
+def oauth_login(request):
+ """Step 1: User Authorization.
+ Redirect the user/resource owner to the OAuth provider (i.e. Github)
+ using an URL with a few key OAuth parameters.
+ """
+ try:
+ authorization_url, state = session.authorization_url(
+ AUTHORIZATION_URL)
+ except Exception as e:
+ logger.error(e)
+ return render_to_response('error.html', {
+ 'error_msg': _('Error, please contact administrator.'),
+ }, context_instance=RequestContext(request))
+
+ return HttpResponseRedirect(authorization_url)
+
+# Step 2: User authorization, this happens on the provider.
+
+@oauth_check
+def oauth_callback(request):
+ """ Step 3: Retrieving an access token.
+ The user has been redirected back from the provider to your registered
+ callback URL. With this redirection comes an authorization code included
+ in the redirect URL. We will use that to obtain an access token.
+ """
+ try:
+ session.fetch_token(TOKEN_URL, client_secret=CLIENT_SECRET,
+ authorization_response=request.get_full_path())
+ user_info_resp = session.get(USER_INFO_URL)
+ except Exception as e:
+ logger.error(e)
+ return render_to_response('error.html', {
+ 'error_msg': _('Error, please contact administrator.'),
+ }, context_instance=RequestContext(request))
+
+ def format_user_info(user_info_resp):
+
+ error = False
+ user_info = {}
+ user_info_json = user_info_resp.json()
+
+ for item, attr in ATTRIBUTE_MAP.items():
+ required, user_attr = attr
+ value = str(user_info_json.get(item, ''))
+
+ # ccnet email
+ if user_attr == 'email':
+ user_info[user_attr] = value if is_valid_email(value) else \
+ '%s@%s' % (value, PROVIDER_DOMAIN)
+ else:
+ user_info[user_attr] = value
+
+ if required and not value:
+ error = True
+
+ return user_info, error
+
+ user_info, error = format_user_info(user_info_resp)
+ if error:
+ logger.error('Required user info not found.')
+ logger.error(user_info)
+ return render_to_response('error.html', {
+ 'error_msg': _('Error, please contact administrator.'),
+ }, context_instance=RequestContext(request))
+
+ # seahub authenticate user
+ email = user_info['email']
+ user = auth.authenticate(remote_user=email)
+
+ if not user or not user.is_active:
+ logger.error('User %s not found or inactive.' % email)
+ # a page for authenticate user failed
+ return render_to_response('error.html', {
+ 'error_msg': _(u'User %s not found.') % email
+ }, context_instance=RequestContext(request))
+
+ # User is valid. Set request.user and persist user in the session
+ # by logging the user in.
+ request.user = user
+ auth.login(request, user)
+ user.set_unusable_password()
+ user.save()
+
+ # update user's profile
+ name = user_info['name'] if user_info.has_key('name') else ''
+ contact_email = user_info['contact_email'] if \
+ user_info.has_key('contact_email') else ''
+
+ profile = Profile.objects.get_profile_by_user(email)
+ if not profile:
+ profile = Profile(user=email)
+
+ if name:
+ profile.nickname = name.strip()
+
+ if contact_email:
+ profile.contact_email = contact_email.strip()
+
+ profile.save()
+
+ # redirect user to home page
+ return HttpResponseRedirect(reverse('libraries'))
diff --git a/seahub/settings.py b/seahub/settings.py
index 0de35c0496..31abdbb971 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -240,7 +240,12 @@ CONSTANCE_DATABASE_CACHE_BACKEND = 'default'
AUTHENTICATION_BACKENDS = (
'seahub.base.accounts.AuthBackend',
+ 'seahub.oauth.backends.OauthRemoteUserBackend',
+
)
+
+ENABLE_OAUTH = False
+
LOGIN_REDIRECT_URL = '/profile/'
LOGIN_URL = SITE_ROOT + 'accounts/login'
LOGOUT_REDIRECT_URL = None
diff --git a/seahub/templates/registration/login.html b/seahub/templates/registration/login.html
index f646216161..f32c862885 100644
--- a/seahub/templates/registration/login.html
+++ b/seahub/templates/registration/login.html
@@ -66,6 +66,10 @@ html, body, #wrapper { height:100%; }
+ {% if enable_oauth %}
+ {% trans "Single Sign-On" %}
+ {% endif %}
+
{% if enable_adfs_login %}
ADFS
{% endif %}
diff --git a/seahub/urls.py b/seahub/urls.py
index e2ef5a84ce..6d4221888b 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -110,6 +110,8 @@ urlpatterns = patterns(
(r'^sso/$', sso),
url(r'^shib-login/', shib_login, name="shib_login"),
+ (r'^oauth/', include('seahub.oauth.urls')),
+
url(r'^$', libraries, name='libraries'),
#url(r'^home/$', direct_to_template, { 'template': 'home.html' } ),
url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),