Commit 26b3b3ce authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

Google and LinkedIn oauth support

parent 4180770d
......@@ -151,6 +151,7 @@ class LocalAuthProvider(AuthProvider):
def extra_actions(self):
return [(_('Change password'), reverse('password_change')), ]
class LDAPAuthProvider(AuthProvider):
module = 'ldap'
title = _('LDAP credentials')
......@@ -188,8 +189,8 @@ class ShibbolethAuthProvider(AuthProvider):
class TwitterAuthProvider(AuthProvider):
module = 'twitter'
title = _('Twitter')
description = _('Allows you to login to your account using your twitter '
'account')
description = _('Allows you to login to your account using your Twitter '
'credentials')
add_prompt = _('Connect with your Twitter account.')
details_tpl = _('Twitter screen name: %(info_screen_name)s')
user_title = _('Twitter (%(info_screen_name)s)')
......@@ -201,6 +202,41 @@ class TwitterAuthProvider(AuthProvider):
login_template = 'im/auth/twitter_login.html'
login_prompt_template = 'im/auth/twitter_login_prompt.html'
class GoogleAuthProvider(AuthProvider):
module = 'google'
title = _('Google')
description = _('Allows you to login to your account using your Google '
'credentials')
add_prompt = _('Connect with your Google account.')
details_tpl = _('Google account: %(info_email)s')
user_title = _('Google (%(info_email)s)')
@property
def add_url(self):
return reverse('astakos.im.target.google.login')
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
class LinkedInAuthProvider(AuthProvider):
module = 'linkedin'
title = _('LinkedIn')
description = _('Allows you to login to your account using your LinkedIn '
'credentials')
add_prompt = _('Connect with your LinkedIn account.')
details_tpl = _('LinkedIn account: %(info_emailAddress)s')
user_title = _('LinkedIn (%(info_emailAddress)s)')
@property
def add_url(self):
return reverse('astakos.im.target.linkedin.login')
login_template = 'im/auth/third_party_provider_generic_login.html'
login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
def get_provider(id, user_obj=None, default=None):
"""
Return a provider instance from the auth providers registry.
......
......@@ -10,6 +10,15 @@ TWITTER_SECRET = getattr(settings, 'ASTAKOS_TWITTER_SECRET', '')
TWITTER_AUTH_FORCE_LOGIN = getattr(settings, 'ASTAKOS_TWITTER_AUTH_FORCE_LOGIN',
False)
# OAuth2 Google credentials.
GOOGLE_CLIENT_ID = getattr(settings, 'ASTAKOS_GOOGLE_CLIENT_ID', '')
GOOGLE_SECRET = getattr(settings, 'ASTAKOS_GOOGLE_SECRET', '')
# OAuth2 LinkedIn credentials.
LINKEDIN_TOKEN = getattr(settings, 'ASTAKOS_LINKEDIN_TOKEN', '')
LINKEDIN_SECRET = getattr(settings, 'ASTAKOS_LINKEDIN_SECRET', '')
DEFAULT_USER_LEVEL = getattr(settings, 'ASTAKOS_DEFAULT_USER_LEVEL', 4)
INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', {
......
# Copyright 2011-2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import json
from django.http import HttpResponseBadRequest
from django.utils.translation import ugettext as _
from django.contrib import messages
from django.template import RequestContext
from django.views.decorators.http import require_http_methods
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from urlparse import urlunsplit, urlsplit
from astakos.im.util import prepare_response, get_context
from astakos.im.views import requires_anonymous, render_response, \
requires_auth_provider
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
from astakos.im.models import AstakosUser, PendingThirdPartyUser
from astakos.im.forms import LoginForm
from astakos.im.activation_backends import get_backend, SimpleBackend
from astakos.im import settings
from astakos.im import auth_providers
import logging
import time
import astakos.im.messages as astakos_messages
import urlparse
import urllib
logger = logging.getLogger(__name__)
import oauth2 as oauth
import cgi
signature_method = oauth.SignatureMethod_HMAC_SHA1()
OAUTH_CONSUMER_KEY = settings.GOOGLE_CLIENT_ID
OAUTH_CONSUMER_SECRET = settings.GOOGLE_SECRET
consumer = oauth.Consumer(key=OAUTH_CONSUMER_KEY, secret=OAUTH_CONSUMER_SECRET)
client = oauth.Client(consumer)
token_scope = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'
authenticate_url = 'https://accounts.google.com/o/oauth2/auth'
access_token_url = 'https://www.googleapis.com/oauth2/v1/tokeninfo'
request_token_url = 'https://accounts.google.com/o/oauth2/token'
def get_redirect_uri():
return "%s%s" % (settings.BASEURL,
reverse('astakos.im.target.google.authenticated'))
@requires_auth_provider('google', login=True)
@require_http_methods(["GET", "POST"])
def login(request):
params = {
'scope': token_scope,
'response_type': 'code',
'redirect_uri': get_redirect_uri(),
'client_id': settings.GOOGLE_CLIENT_ID
}
url = "%s?%s" % (authenticate_url, urllib.urlencode(params))
return HttpResponseRedirect(url)
@requires_auth_provider('google', login=True)
@require_http_methods(["GET", "POST"])
def authenticated(
request,
template='im/third_party_check_local.html',
extra_context={}
):
# TODO: Handle errors, e.g. error=access_denied
try:
code = request.GET.get('code', None)
params = {
'code': code,
'client_id': settings.GOOGLE_CLIENT_ID,
'client_secret': settings.GOOGLE_SECRET,
'redirect_uri': get_redirect_uri(),
'grant_type': 'authorization_code'
}
get_token_url = "%s" % (request_token_url,)
resp, content = client.request(get_token_url, "POST",
body=urllib.urlencode(params))
token = json.loads(content).get('access_token', None)
resp, content = client.request("%s?access_token=%s" % (access_token_url,
token) , "GET")
access_token_data = json.loads(content)
except Exception, e:
messages.error(request, 'Invalid Google response. Please contact support')
return HttpResponseRedirect(reverse('edit_profile'))
userid = access_token_data['user_id']
username = access_token_data.get('email', None)
provider_info = access_token_data
affiliation = 'Google.com'
# an existing user accessed the view
if request.user.is_authenticated():
if request.user.has_auth_provider('google', identifier=userid):
return HttpResponseRedirect(reverse('edit_profile'))
# automatically add eppn provider to user
user = request.user
if not request.user.can_add_auth_provider('google',
identifier=userid):
messages.error(request, _(astakos_messages.AUTH_PROVIDER_ADD_FAILED) +
u' ' + _(astakos_messages.AUTH_PROVIDER_ADD_EXISTS))
return HttpResponseRedirect(reverse('edit_profile'))
user.add_auth_provider('google', identifier=userid,
affiliation=affiliation,
provider_info=provider_info)
messages.success(request, astakos_messages.AUTH_PROVIDER_ADDED)
return HttpResponseRedirect(reverse('edit_profile'))
try:
# astakos user exists ?
user = AstakosUser.objects.get_auth_provider_user(
'google',
identifier=userid
)
if user.is_active:
# authenticate user
response = prepare_response(request,
user,
request.GET.get('next'),
'renew' in request.GET)
response.set_cookie('astakos_last_login_method', 'google')
return response
else:
message = user.get_inactive_message()
messages.error(request, message)
return HttpResponseRedirect(reverse('login'))
except AstakosUser.DoesNotExist, e:
provider = auth_providers.get_provider('google')
if not provider.is_available_for_create():
messages.error(request,
_(astakos_messages.AUTH_PROVIDER_NOT_ACTIVE) % provider.get_title_display)
return HttpResponseRedirect(reverse('login'))
# eppn not stored in astakos models, create pending profile
user, created = PendingThirdPartyUser.objects.get_or_create(
third_party_identifier=userid,
provider='google',
)
# update pending user
user.affiliation = affiliation
user.info = json.dumps(provider_info)
user.generate_token()
user.save()
extra_context['provider'] = 'google'
extra_context['provider_title'] = 'google'
extra_context['token'] = user.token
extra_context['signup_url'] = reverse('signup') + \
"?third_party_token=%s" % user.token
return render_response(
template,
context_instance=get_context(request, extra_context)
)
# Copyright 2011-2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import json
from django.http import HttpResponseBadRequest
from django.utils.translation import ugettext as _
from django.contrib import messages
from django.template import RequestContext
from django.views.decorators.http import require_http_methods
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import get_object_or_404
from urlparse import urlunsplit, urlsplit
from astakos.im.util import prepare_response, get_context
from astakos.im.views import requires_anonymous, render_response, \
requires_auth_provider
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
from astakos.im.models import AstakosUser, PendingThirdPartyUser
from astakos.im.forms import LoginForm
from astakos.im.activation_backends import get_backend, SimpleBackend
from astakos.im import settings
from astakos.im import auth_providers
import astakos.im.messages as astakos_messages
import logging
logger = logging.getLogger(__name__)
import oauth2 as oauth
import cgi
consumer = oauth.Consumer(settings.LINKEDIN_TOKEN, settings.LINKEDIN_SECRET)
client = oauth.Client(consumer)
request_token_url = 'https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress'
access_token_url = 'https://api.linkedin.com/uas/oauth/accessToken'
authenticate_url = 'https://www.linkedin.com/uas/oauth/authorize'
@requires_auth_provider('linkedin', login=True)
@require_http_methods(["GET", "POST"])
def login(request):
resp, content = client.request(request_token_url, "GET")
if resp['status'] != '200':
messages.error(request, 'Invalid linkedin response')
return HttpResponseRedirect(reverse('edit_profile'))
print "111111111111", "RESP", "CONTENT", resp, content
request_token = dict(cgi.parse_qsl(content))
print request_token
request.session['request_token'] = request_token
url = request_token.get('xoauth_request_auth_url') + "?oauth_token=%s" % request_token.get('oauth_token')
return HttpResponseRedirect(url)
@requires_auth_provider('linkedin', login=True)
@require_http_methods(["GET", "POST"])
def authenticated(
request,
template='im/third_party_check_local.html',
extra_context={}
):
if request.GET.get('denied'):
return HttpResponseRedirect(reverse('edit_profile'))
if not 'request_token' in request.session:
messages.error(request, 'linkedin handshake failed')
return HttpResponseRedirect(reverse('edit_profile'))
token = oauth.Token(request.session['request_token']['oauth_token'],
request.session['request_token']['oauth_token_secret'])
token.set_verifier(request.GET.get('oauth_verifier'))
client = oauth.Client(consumer, token)
resp, content = client.request(access_token_url, "POST")
if resp['status'] != '200':
try:
del request.session['request_token']
except:
pass
messages.error(request, 'Invalid linkedin token response')
return HttpResponseRedirect(reverse('edit_profile'))
access_token = dict(cgi.parse_qsl(content))
print "ACCESS", access_token
token = oauth.Token(access_token['oauth_token'],
access_token['oauth_token_secret'])
client = oauth.Client(consumer, token)
resp, content = client.request("http://api.linkedin.com/v1/people/~:(id,first-name,last-name,industry,email-address)?format=json", "GET")
if resp['status'] != '200':
print resp, content
try:
del request.session['request_token']
except:
pass
messages.error(request, 'Invalid linkedin profile response')
return HttpResponseRedirect(reverse('edit_profile'))
profile_data = json.loads(content)
userid = profile_data['id']
username = profile_data.get('emailAddress', None)
realname = profile_data.get('firstName', '') + ' ' + profile_data.get('lastName', '')
provider_info = profile_data
affiliation = 'linkedin.com'
# an existing user accessed the view
if request.user.is_authenticated():
if request.user.has_auth_provider('linkedin', identifier=userid):
return HttpResponseRedirect(reverse('edit_profile'))
# automatically add eppn provider to user
user = request.user
if not request.user.can_add_auth_provider('linkedin',
identifier=userid):
messages.error(request, _(astakos_messages.AUTH_PROVIDER_ADD_FAILED) +
u' ' + _(astakos_messages.AUTH_PROVIDER_ADD_EXISTS))
return HttpResponseRedirect(reverse('edit_profile'))
user.add_auth_provider('linkedin', identifier=userid,
affiliation=affiliation,
provider_info=provider_info)
messages.success(request, astakos_messages.AUTH_PROVIDER_ADDED)
return HttpResponseRedirect(reverse('edit_profile'))
try:
# astakos user exists ?
user = AstakosUser.objects.get_auth_provider_user(
'linkedin',
identifier=userid
)
if user.is_active:
# authenticate user
response = prepare_response(request,
user,
request.GET.get('next'),
'renew' in request.GET)
response.set_cookie('astakos_last_login_method', 'linkedin')
return response
else:
message = user.get_inactive_message()
messages.error(request, message)
return HttpResponseRedirect(reverse('login'))
except AstakosUser.DoesNotExist, e:
provider = auth_providers.get_provider('linkedin')
if not provider.is_available_for_create():
messages.error(request,
_(astakos_messages.AUTH_PROVIDER_NOT_ACTIVE) % provider.get_title_display)
return HttpResponseRedirect(reverse('login'))
# eppn not stored in astakos models, create pending profile
user, created = PendingThirdPartyUser.objects.get_or_create(
third_party_identifier=userid,
provider='linkedin',
)
# update pending user
user.realname = realname
user.affiliation = affiliation
user.info = json.dumps(provider_info)
user.generate_token()
user.save()
extra_context['provider'] = 'linkedin'
extra_context['provider_title'] = 'linkedin'
extra_context['token'] = user.token
extra_context['signup_url'] = reverse('signup') + \
"?third_party_token=%s" % user.token
return render_response(
template,
context_instance=get_context(request, extra_context)
)
<h2><a href="{{ provider.add_url }}">{{ provider.get_login_prompt_display }} {{ provider.get_title_display }}</a></h2>
<br />{{ provider.get_login_prompt_display }}
<a href="{{ provider.add_url }}?{% ifnotequal next "" %}&next={{ next|urlencode }}{% endifnotequal %}{% ifnotequal code ""%}{% if next != "" %}&{% else %}?{% endif %}code={{ code }}{% endifnotequal %}"
alt="{{ provider.get_title_display }}">{{ provider.get_title_display }}</a>
......@@ -124,6 +124,19 @@ if 'twitter' in IM_MODULES:
'twitter.authenticated'),
)
if 'google' in IM_MODULES:
urlpatterns += patterns('astakos.im.target',
url(r'^login/goggle/?$', 'google.login'),
url(r'^login/google/authenticated/?$',
'google.authenticated'),
)
if 'linkedin' in IM_MODULES:
urlpatterns += patterns('astakos.im.target',
url(r'^login/linkedin/?$', 'linkedin.login'),
url(r'^login/linkedin/authenticated/?$',
'linkedin.authenticated'),
)
urlpatterns += patterns('astakos.im.api',
url(r'^get_services/?$', 'get_services'),
url(r'^get_menu/?$', 'get_menu'),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment