Revision 74796dd8

b/snf-astakos-app/astakos/im/auth_providers.py
151 151
    def extra_actions(self):
152 152
        return [(_('Change password'), reverse('password_change')), ]
153 153

  
154

  
154 155
class LDAPAuthProvider(AuthProvider):
155 156
    module = 'ldap'
156 157
    title = _('LDAP credentials')
......
188 189
class TwitterAuthProvider(AuthProvider):
189 190
    module = 'twitter'
190 191
    title = _('Twitter')
191
    description = _('Allows you to login to your account using your twitter '
192
                    'account')
192
    description = _('Allows you to login to your account using your Twitter '
193
                    'credentials')
193 194
    add_prompt = _('Connect with your Twitter account.')
194 195
    details_tpl = _('Twitter screen name: %(info_screen_name)s')
195 196
    user_title = _('Twitter (%(info_screen_name)s)')
......
201 202
    login_template = 'im/auth/twitter_login.html'
202 203
    login_prompt_template = 'im/auth/twitter_login_prompt.html'
203 204

  
205

  
206
class GoogleAuthProvider(AuthProvider):
207
    module = 'google'
208
    title = _('Google')
209
    description = _('Allows you to login to your account using your Google '
210
                    'credentials')
211
    add_prompt = _('Connect with your Google account.')
212
    details_tpl = _('Google account: %(info_email)s')
213
    user_title = _('Google (%(info_email)s)')
214

  
215
    @property
216
    def add_url(self):
217
        return reverse('astakos.im.target.google.login')
218

  
219
    login_template = 'im/auth/third_party_provider_generic_login.html'
220
    login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
221

  
222

  
223
class LinkedInAuthProvider(AuthProvider):
224
    module = 'linkedin'
225
    title = _('LinkedIn')
226
    description = _('Allows you to login to your account using your LinkedIn '
227
                    'credentials')
228
    add_prompt = _('Connect with your LinkedIn account.')
229
    details_tpl = _('LinkedIn account: %(info_emailAddress)s')
230
    user_title = _('LinkedIn (%(info_emailAddress)s)')
231

  
232
    @property
233
    def add_url(self):
234
        return reverse('astakos.im.target.linkedin.login')
235

  
236
    login_template = 'im/auth/third_party_provider_generic_login.html'
237
    login_prompt_template = 'im/auth/third_party_provider_generic_login_prompt.html'
238

  
239

  
204 240
def get_provider(id, user_obj=None, default=None):
205 241
    """
206 242
    Return a provider instance from the auth providers registry.
b/snf-astakos-app/astakos/im/settings.py
10 10
TWITTER_AUTH_FORCE_LOGIN = getattr(settings, 'ASTAKOS_TWITTER_AUTH_FORCE_LOGIN',
11 11
                                  False)
12 12

  
13

  
14
# OAuth2 Google credentials.
15
GOOGLE_CLIENT_ID = getattr(settings, 'ASTAKOS_GOOGLE_CLIENT_ID', '')
16
GOOGLE_SECRET = getattr(settings, 'ASTAKOS_GOOGLE_SECRET', '')
17

  
18
# OAuth2 LinkedIn credentials.
19
LINKEDIN_TOKEN = getattr(settings, 'ASTAKOS_LINKEDIN_TOKEN', '')
20
LINKEDIN_SECRET = getattr(settings, 'ASTAKOS_LINKEDIN_SECRET', '')
21

  
13 22
DEFAULT_USER_LEVEL = getattr(settings, 'ASTAKOS_DEFAULT_USER_LEVEL', 4)
14 23

  
15 24
INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', {
b/snf-astakos-app/astakos/im/target/google.py
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import json
35

  
36
from django.http import HttpResponseBadRequest
37
from django.utils.translation import ugettext as _
38
from django.contrib import messages
39
from django.template import RequestContext
40
from django.views.decorators.http import require_http_methods
41
from django.http import HttpResponseRedirect
42
from django.core.urlresolvers import reverse
43
from django.core.exceptions import ImproperlyConfigured
44
from django.shortcuts import get_object_or_404
45

  
46
from urlparse import urlunsplit, urlsplit
47

  
48
from astakos.im.util import prepare_response, get_context
49
from astakos.im.views import requires_anonymous, render_response, \
50
        requires_auth_provider
51
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
52
from astakos.im.models import AstakosUser, PendingThirdPartyUser
53
from astakos.im.forms import LoginForm
54
from astakos.im.activation_backends import get_backend, SimpleBackend
55
from astakos.im import settings
56
from astakos.im import auth_providers
57

  
58
import logging
59
import time
60
import astakos.im.messages as astakos_messages
61
import urlparse
62
import urllib
63

  
64
logger = logging.getLogger(__name__)
65

  
66
import oauth2 as oauth
67
import cgi
68

  
69
signature_method = oauth.SignatureMethod_HMAC_SHA1()
70

  
71
OAUTH_CONSUMER_KEY = settings.GOOGLE_CLIENT_ID
72
OAUTH_CONSUMER_SECRET = settings.GOOGLE_SECRET
73

  
74
consumer = oauth.Consumer(key=OAUTH_CONSUMER_KEY, secret=OAUTH_CONSUMER_SECRET)
75
client = oauth.Client(consumer)
76

  
77
token_scope = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'
78
authenticate_url = 'https://accounts.google.com/o/oauth2/auth'
79
access_token_url = 'https://www.googleapis.com/oauth2/v1/tokeninfo'
80
request_token_url = 'https://accounts.google.com/o/oauth2/token'
81

  
82

  
83
def get_redirect_uri():
84
    return "%s%s" % (settings.BASEURL,
85
                   reverse('astakos.im.target.google.authenticated'))
86

  
87
@requires_auth_provider('google', login=True)
88
@require_http_methods(["GET", "POST"])
89
def login(request):
90
    params = {
91
        'scope': token_scope,
92
        'response_type': 'code',
93
        'redirect_uri': get_redirect_uri(),
94
        'client_id': settings.GOOGLE_CLIENT_ID
95
    }
96
    url = "%s?%s" % (authenticate_url, urllib.urlencode(params))
97
    return HttpResponseRedirect(url)
98

  
99

  
100
@requires_auth_provider('google', login=True)
101
@require_http_methods(["GET", "POST"])
102
def authenticated(
103
    request,
104
    template='im/third_party_check_local.html',
105
    extra_context={}
106
):
107

  
108
    # TODO: Handle errors, e.g. error=access_denied
109
    try:
110
        code = request.GET.get('code', None)
111
        params = {
112
            'code': code,
113
            'client_id': settings.GOOGLE_CLIENT_ID,
114
            'client_secret': settings.GOOGLE_SECRET,
115
            'redirect_uri': get_redirect_uri(),
116
            'grant_type': 'authorization_code'
117
        }
118
        get_token_url = "%s" % (request_token_url,)
119
        resp, content = client.request(get_token_url, "POST",
120
                                       body=urllib.urlencode(params))
121
        token = json.loads(content).get('access_token', None)
122

  
123
        resp, content = client.request("%s?access_token=%s" % (access_token_url,
124
                                                               token) , "GET")
125
        access_token_data = json.loads(content)
126
    except Exception, e:
127
        messages.error(request, 'Invalid Google response. Please contact support')
128
        return HttpResponseRedirect(reverse('edit_profile'))
129

  
130
    userid = access_token_data['user_id']
131
    username = access_token_data.get('email', None)
132
    provider_info = access_token_data
133
    affiliation = 'Google.com'
134

  
135
    # an existing user accessed the view
136
    if request.user.is_authenticated():
137
        if request.user.has_auth_provider('google', identifier=userid):
138
            return HttpResponseRedirect(reverse('edit_profile'))
139

  
140
        # automatically add eppn provider to user
141
        user = request.user
142
        if not request.user.can_add_auth_provider('google',
143
                                                  identifier=userid):
144
            messages.error(request, _(astakos_messages.AUTH_PROVIDER_ADD_FAILED) +
145
                          u' ' + _(astakos_messages.AUTH_PROVIDER_ADD_EXISTS))
146
            return HttpResponseRedirect(reverse('edit_profile'))
147

  
148
        user.add_auth_provider('google', identifier=userid,
149
                               affiliation=affiliation,
150
                               provider_info=provider_info)
151
        messages.success(request, astakos_messages.AUTH_PROVIDER_ADDED)
152
        return HttpResponseRedirect(reverse('edit_profile'))
153

  
154
    try:
155
        # astakos user exists ?
156
        user = AstakosUser.objects.get_auth_provider_user(
157
            'google',
158
            identifier=userid
159
        )
160
        if user.is_active:
161
            # authenticate user
162
            response = prepare_response(request,
163
                                    user,
164
                                    request.GET.get('next'),
165
                                    'renew' in request.GET)
166
            response.set_cookie('astakos_last_login_method', 'google')
167
            return response
168
        else:
169
            message = user.get_inactive_message()
170
            messages.error(request, message)
171
            return HttpResponseRedirect(reverse('login'))
172

  
173
    except AstakosUser.DoesNotExist, e:
174
        provider = auth_providers.get_provider('google')
175
        if not provider.is_available_for_create():
176
            messages.error(request,
177
                           _(astakos_messages.AUTH_PROVIDER_NOT_ACTIVE) % provider.get_title_display)
178
            return HttpResponseRedirect(reverse('login'))
179

  
180
        # eppn not stored in astakos models, create pending profile
181
        user, created = PendingThirdPartyUser.objects.get_or_create(
182
            third_party_identifier=userid,
183
            provider='google',
184
        )
185
        # update pending user
186
        user.affiliation = affiliation
187
        user.info = json.dumps(provider_info)
188
        user.generate_token()
189
        user.save()
190

  
191
        extra_context['provider'] = 'google'
192
        extra_context['provider_title'] = 'google'
193
        extra_context['token'] = user.token
194
        extra_context['signup_url'] = reverse('signup') + \
195
                                    "?third_party_token=%s" % user.token
196

  
197
        return render_response(
198
            template,
199
            context_instance=get_context(request, extra_context)
200
        )
201

  
202

  
b/snf-astakos-app/astakos/im/target/linkedin.py
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
import json
35

  
36
from django.http import HttpResponseBadRequest
37
from django.utils.translation import ugettext as _
38
from django.contrib import messages
39
from django.template import RequestContext
40
from django.views.decorators.http import require_http_methods
41
from django.http import HttpResponseRedirect
42
from django.core.urlresolvers import reverse
43
from django.core.exceptions import ImproperlyConfigured
44
from django.shortcuts import get_object_or_404
45

  
46
from urlparse import urlunsplit, urlsplit
47

  
48
from astakos.im.util import prepare_response, get_context
49
from astakos.im.views import requires_anonymous, render_response, \
50
        requires_auth_provider
51
from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
52
from astakos.im.models import AstakosUser, PendingThirdPartyUser
53
from astakos.im.forms import LoginForm
54
from astakos.im.activation_backends import get_backend, SimpleBackend
55
from astakos.im import settings
56
from astakos.im import auth_providers
57

  
58
import astakos.im.messages as astakos_messages
59

  
60
import logging
61

  
62
logger = logging.getLogger(__name__)
63

  
64
import oauth2 as oauth
65
import cgi
66

  
67
consumer = oauth.Consumer(settings.LINKEDIN_TOKEN, settings.LINKEDIN_SECRET)
68
client = oauth.Client(consumer)
69

  
70
request_token_url      = 'https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress'
71
access_token_url       = 'https://api.linkedin.com/uas/oauth/accessToken'
72
authenticate_url       = 'https://www.linkedin.com/uas/oauth/authorize'
73

  
74

  
75
@requires_auth_provider('linkedin', login=True)
76
@require_http_methods(["GET", "POST"])
77
def login(request):
78
    resp, content = client.request(request_token_url, "GET")
79
    if resp['status'] != '200':
80
        messages.error(request, 'Invalid linkedin response')
81
        return HttpResponseRedirect(reverse('edit_profile'))
82

  
83
    print "111111111111", "RESP", "CONTENT", resp, content
84
    request_token = dict(cgi.parse_qsl(content))
85
    print request_token
86
    request.session['request_token'] = request_token
87

  
88
    url = request_token.get('xoauth_request_auth_url') + "?oauth_token=%s" % request_token.get('oauth_token')
89

  
90
    return HttpResponseRedirect(url)
91

  
92

  
93
@requires_auth_provider('linkedin', login=True)
94
@require_http_methods(["GET", "POST"])
95
def authenticated(
96
    request,
97
    template='im/third_party_check_local.html',
98
    extra_context={}
99
):
100

  
101
    if request.GET.get('denied'):
102
        return HttpResponseRedirect(reverse('edit_profile'))
103

  
104
    if not 'request_token' in request.session:
105
        messages.error(request, 'linkedin handshake failed')
106
        return HttpResponseRedirect(reverse('edit_profile'))
107

  
108
    token = oauth.Token(request.session['request_token']['oauth_token'],
109
        request.session['request_token']['oauth_token_secret'])
110
    token.set_verifier(request.GET.get('oauth_verifier'))
111
    client = oauth.Client(consumer, token)
112
    resp, content = client.request(access_token_url, "POST")
113
    if resp['status'] != '200':
114
        try:
115
            del request.session['request_token']
116
        except:
117
            pass
118
        messages.error(request, 'Invalid linkedin token response')
119
        return HttpResponseRedirect(reverse('edit_profile'))
120
    access_token = dict(cgi.parse_qsl(content))
121
    print "ACCESS", access_token
122

  
123
    token = oauth.Token(access_token['oauth_token'],
124
        access_token['oauth_token_secret'])
125
    client = oauth.Client(consumer, token)
126
    resp, content = client.request("http://api.linkedin.com/v1/people/~:(id,first-name,last-name,industry,email-address)?format=json", "GET")
127
    if resp['status'] != '200':
128
        print resp, content
129
        try:
130
            del request.session['request_token']
131
        except:
132
            pass
133
        messages.error(request, 'Invalid linkedin profile response')
134
        return HttpResponseRedirect(reverse('edit_profile'))
135

  
136
    profile_data = json.loads(content)
137
    userid = profile_data['id']
138
    username = profile_data.get('emailAddress', None)
139
    realname = profile_data.get('firstName', '') + ' ' + profile_data.get('lastName', '')
140
    provider_info = profile_data
141
    affiliation = 'linkedin.com'
142

  
143
    # an existing user accessed the view
144
    if request.user.is_authenticated():
145
        if request.user.has_auth_provider('linkedin', identifier=userid):
146
            return HttpResponseRedirect(reverse('edit_profile'))
147

  
148
        # automatically add eppn provider to user
149
        user = request.user
150
        if not request.user.can_add_auth_provider('linkedin',
151
                                                  identifier=userid):
152
            messages.error(request, _(astakos_messages.AUTH_PROVIDER_ADD_FAILED) +
153
                          u' ' + _(astakos_messages.AUTH_PROVIDER_ADD_EXISTS))
154
            return HttpResponseRedirect(reverse('edit_profile'))
155

  
156
        user.add_auth_provider('linkedin', identifier=userid,
157
                               affiliation=affiliation,
158
                               provider_info=provider_info)
159
        messages.success(request, astakos_messages.AUTH_PROVIDER_ADDED)
160
        return HttpResponseRedirect(reverse('edit_profile'))
161

  
162
    try:
163
        # astakos user exists ?
164
        user = AstakosUser.objects.get_auth_provider_user(
165
            'linkedin',
166
            identifier=userid
167
        )
168
        if user.is_active:
169
            # authenticate user
170
            response = prepare_response(request,
171
                                    user,
172
                                    request.GET.get('next'),
173
                                    'renew' in request.GET)
174
            response.set_cookie('astakos_last_login_method', 'linkedin')
175
            return response
176
        else:
177
            message = user.get_inactive_message()
178
            messages.error(request, message)
179
            return HttpResponseRedirect(reverse('login'))
180

  
181
    except AstakosUser.DoesNotExist, e:
182
        provider = auth_providers.get_provider('linkedin')
183
        if not provider.is_available_for_create():
184
            messages.error(request,
185
                           _(astakos_messages.AUTH_PROVIDER_NOT_ACTIVE) % provider.get_title_display)
186
            return HttpResponseRedirect(reverse('login'))
187

  
188
        # eppn not stored in astakos models, create pending profile
189
        user, created = PendingThirdPartyUser.objects.get_or_create(
190
            third_party_identifier=userid,
191
            provider='linkedin',
192
        )
193
        # update pending user
194
        user.realname = realname
195
        user.affiliation = affiliation
196
        user.info = json.dumps(provider_info)
197
        user.generate_token()
198
        user.save()
199

  
200
        extra_context['provider'] = 'linkedin'
201
        extra_context['provider_title'] = 'linkedin'
202
        extra_context['token'] = user.token
203
        extra_context['signup_url'] = reverse('signup') + \
204
                                    "?third_party_token=%s" % user.token
205

  
206
        return render_response(
207
            template,
208
            context_instance=get_context(request, extra_context)
209
        )
210

  
211

  
212

  
b/snf-astakos-app/astakos/im/templates/im/auth/third_party_provider_generic_login.html
1
<h2><a href="{{ provider.add_url }}">{{ provider.get_login_prompt_display }} {{ provider.get_title_display }}</a></h2>
b/snf-astakos-app/astakos/im/templates/im/auth/third_party_provider_generic_login_prompt.html
1
<br />{{ provider.get_login_prompt_display }}
2
<a href="{{ provider.add_url }}?{% ifnotequal next "" %}&next={{ next|urlencode }}{% endifnotequal %}{% ifnotequal code ""%}{% if next != "" %}&{% else %}?{% endif %}code={{ code }}{% endifnotequal %}"
3
  alt="{{ provider.get_title_display }}">{{ provider.get_title_display }}</a>
b/snf-astakos-app/astakos/im/urls.py
124 124
                                'twitter.authenticated'),
125 125
                            )
126 126

  
127
if 'google' in IM_MODULES:
128
    urlpatterns += patterns('astakos.im.target',
129
                            url(r'^login/goggle/?$', 'google.login'),
130
                            url(r'^login/google/authenticated/?$',
131
                                'google.authenticated'),
132
                            )
133
if 'linkedin' in IM_MODULES:
134
    urlpatterns += patterns('astakos.im.target',
135
                            url(r'^login/linkedin/?$', 'linkedin.login'),
136
                            url(r'^login/linkedin/authenticated/?$',
137
                                'linkedin.authenticated'),
138
                            )
139

  
127 140
urlpatterns += patterns('astakos.im.api',
128 141
                        url(r'^get_services/?$', 'get_services'),
129 142
                        url(r'^get_menu/?$', 'get_menu'),

Also available in: Unified diff