/SetupIdp
85 |
86 | You might need to register at http://code.google.com/apis/accounts/docs/RegistrationForWebAppsAuto.html
87 | Might look like this: http://static.23.nu/md/Pictures/ZZ6F76B85B.png
88 |
89 | If google claims "invalid page" check GOOGLE_OPENID_REALM.
90 |
--------------------------------------------------------------------------------
/googleappsauth/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/__init__.py
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009, 2010 HUDORA GmbH. All rights reserved.
8 |
9 | To use it configuration in settings.py should look like this (this is also in global_django_settings):
10 |
11 | GOOGLE_APPS_DOMAIN = 'hudora.de'
12 | GOOGLE_APPS_CONSUMER_KEY = 'hudora.de'
13 | GOOGLE_APPS_CONSUMER_SECRET = '*sekret*'
14 | GOOGLE_API_SCOPE = 'http://www.google.com/m8/feeds/+http://docs.google.com/feeds/+http://spreadsheets.google.com/feeds/'
15 |
16 | You also have to set the domain where your application is running
17 | GOOGLE_OPENID_REALM = 'http://*.hudora.biz/'
18 |
19 | Then you have to tell where various views live.
20 | LOGIN_REDIRECT_URL = '/admin'
21 |
22 | To activate the whole thing set the appropriate Authentication backend and include a callback view.
23 |
24 | settings.py:
25 | AUTHENTICATION_BACKENDS = ('googleappsauth.backends.GoogleAuthBackend',)
26 | urls.py:
27 | (r'^callback_googleappsauth/', 'googleappsauth.views.callback'),
28 |
29 |
30 | Using a special middleware you can block access to a compete site.
31 |
32 | MIDDLEWARE_CLASSES = (
33 | 'django.middleware.common.CommonMiddleware',
34 | 'django.contrib.sessions.middleware.SessionMiddleware',
35 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
36 | 'hudoratools.googleauth.middleware.GoogleAuthMiddleware',
37 | )
38 |
39 | In addition you can set AUTH_PROTECTED_AREAS to authenticate only access to certain parts of a site, e.g.
40 |
41 | AUTH_PROTECTED_AREAS = ['/admin']
42 | """
43 |
--------------------------------------------------------------------------------
/googleappsauth/backends.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/backends.py - Django authentication backend connecting to Google Apps
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009 HUDORA GmbH. All rights reserved.
8 | """
9 |
10 | from datetime import datetime
11 | from django.conf import settings
12 | from django.contrib.admin.models import LogEntry, ADDITION
13 | from django.contrib.auth.backends import ModelBackend
14 | from django.contrib.auth.models import User, SiteProfileNotAvailable
15 | from django.contrib.contenttypes.models import ContentType
16 | from django.db import models
17 | import re
18 |
19 |
20 | class GoogleAuthBackend(ModelBackend):
21 | def authenticate(self, identifier=None, attributes=None):
22 | # da wir von Google keinen Benutzernamen bekommen versuchen wir zuerst,
23 | # den ersten Teil der Emailadresse zu nehmen. Wenn wir keine Email haben
24 | # dann bleibt nur der OpenID-Identifier als Benutzername
25 | email = attributes.get('email', '')
26 | username = attributes.get('email', identifier).split('@')[0].replace('.', '')
27 | users = User.objects.filter(username=username)
28 | if len(users) > 1:
29 | raise RuntimeError("duplicate user %s" % email)
30 | elif len(users) < 1:
31 | # for some reason it seems this code branch is never executed ?!?
32 | user = User.objects.create(email=email, username=username)
33 | # fuer einen neuen Benutzer erzeugen wir hier ein Zufallspasswort,
34 | # sodass er sich nicht mehr anders als ueber Google Apps einloggen kann
35 | user.set_unusable_password()
36 | # note creation in log
37 | LogEntry.objects.log_action(1, ContentType.objects.get_for_model(User).id,
38 | user.id, unicode(User),
39 | ADDITION, "durch googleauth automatisch erzeugt")
40 | else:
41 | user = users[0]
42 | # jetzt aktualisieren wir die Attribute des Benutzers mit den neuesten
43 | # Werten von Google, falls sich da was geaendert haben sollte
44 | user.first_name = attributes.get('firstname')
45 | user.last_name = attributes.get('lastname')
46 | user.username = username
47 | user.is_staff = True
48 | if not user.password:
49 | user.set_unusable_password()
50 |
51 | user.save()
52 |
53 | # schliesslich speichern wir das Access Token des Benutzers in seinem
54 | # User Profile.
55 | try:
56 | profile = self._get_or_create_user_profile(user)
57 | profile.language = attributes.get('language')
58 | profile.access_token = attributes.get('access_token', '')
59 | profile.save()
60 | except SiteProfileNotAvailable:
61 | pass
62 |
63 | # das war's, Benutzer zurueckliefern, damit ist Login geglueckt
64 | return user
65 |
66 | def get_user(self, user_id):
67 | try:
68 | return User.objects.get(pk=user_id)
69 | except User.DoesNotExist:
70 | return None
71 |
72 | def _get_or_create_user_profile(self, user):
73 | profile_module = getattr(settings, 'AUTH_PROFILE_MODULE', False)
74 | if not profile_module:
75 | raise SiteProfileNotAvailable
76 | app_label, model_name = profile_module.split('.')
77 | model = models.get_model(app_label, model_name)
78 | try:
79 | return user.get_profile()
80 | except model.DoesNotExist:
81 | profile = model()
82 | profile.user = user
83 | return profile
84 |
--------------------------------------------------------------------------------
/googleappsauth/middleware.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/middleware.py - force Google Apps Authentication for the whole site.
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009, 2010 HUDORA GmbH. All rights reserved.
8 | """
9 |
10 | from django.conf import settings
11 | from django.contrib.auth.models import User, SiteProfileNotAvailable
12 | from django.core.exceptions import ImproperlyConfigured
13 | from django.core.urlresolvers import reverse
14 | import django.contrib.auth as djauth
15 | import googleappsauth.views
16 |
17 |
18 | class GoogleAuthMiddleware(object):
19 | """Force Google Apps Authentication for the whole site.
20 |
21 | Using settings.AUTH_PROTECTED_AREAS you can restrict authentication
22 | o only parts of a site.
23 | """
24 |
25 | def process_request(self, request):
26 | # zuerst ueberpruefen wir, ob wir fuer die aktuelle URL
27 | # ueberhaupt einen gueltigen User einloggen muessen
28 | path = request.get_full_path()
29 | areas = getattr(settings, 'AUTH_PROTECTED_AREAS', [])
30 | # LEGACY: AUTH_PROTECTED_AREAS = "foo+bar" - to removed in Version 2.9
31 | if hasattr(areas, 'split'):
32 | areas = areas.split('+')
33 | matches = [area for area in areas if path.startswith(area)]
34 | if len(matches) == 0:
35 | return
36 |
37 | # Don't force authentication for excluded areas - allow sub-folders without auth
38 | excludes = getattr(settings, 'AUTH_EXCLUDED_AREAS', [])
39 | if hasattr(excludes, 'split'):
40 | excludes = excludes.split('+')
41 | exclude_matches = [exclude for exclude in excludes if path.startswith(exclude)]
42 | if len(exclude_matches) != 0:
43 | return
44 |
45 | # Dont force authentication for the callback URL since it would
46 | # result in a loop
47 | callback_url = request.build_absolute_uri(reverse(googleappsauth.views.callback))
48 | callback_path = reverse(googleappsauth.views.callback)
49 | if path.startswith(callback_path):
50 | return
51 |
52 | # ok, die Seite muss auth'd werden. Haben wir vielleicht
53 | # schon einen geauth'd User in der aktuellen Session?
54 | if request.user.is_authenticated():
55 | return
56 |
57 | # nein, wir haben noch keinen User. Also den Login ueber
58 | # Google Apps OpenID/OAuth starten und Parameter in Session speichern
59 | return googleappsauth.views.login(request,
60 | redirect_url="%s?%s" % (path, request.META.get('QUERY_STRING', '')))
61 |
--------------------------------------------------------------------------------
/googleappsauth/oauth.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/oauth.py -
5 |
6 | Created by Axel Schlüter on 2009-12
7 |
8 | code is part of django-twitter-oauth and was taken from http://github.com/henriklied/django-twitter-oauth#.
9 | It was by Henrik Lied and is based on a snippet based on Simon Willison's Fire Eagle views found at
10 | http://www.djangosnippets.org/snippets/655/
11 | """
12 |
13 |
14 | import cgi
15 | import urllib
16 | import time
17 | import random
18 | import urlparse
19 | import hmac
20 | import binascii
21 |
22 | VERSION = '1.0'
23 | HTTP_METHOD = 'GET'
24 | SIGNATURE_METHOD = 'PLAINTEXT'
25 |
26 |
27 | class OAuthError(RuntimeError):
28 | """Generic exception class."""
29 |
30 | def __init__(self, message='OAuth error occured.'):
31 | self.message = message
32 |
33 |
34 | def build_authenticate_header(realm=''):
35 | """optional WWW-Authenticate header (401 error)."""
36 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
37 |
38 |
39 | def escape(s):
40 | """url escape."""
41 | # escape '/' too
42 | return urllib.quote(s, safe='~')
43 |
44 |
45 | def generate_timestamp():
46 | """util function: current timestamp
47 |
48 | seconds since epoch (UTC)"""
49 | return int(time.time())
50 |
51 |
52 | def generate_nonce(length=8):
53 | """util function: nonce
54 | pseudorandom number"""
55 | return ''.join([str(random.randint(0, 9)) for i in range(length)])
56 |
57 |
58 | class OAuthConsumer(object):
59 | """ OAuthConsumer is a data type that represents the identity of the Consumer
60 | via its shared secret with the Service Provider."""
61 | key = None
62 | secret = None
63 |
64 | def __init__(self, key, secret):
65 | self.key = key
66 | self.secret = secret
67 |
68 |
69 | class OAuthToken(object):
70 | """ OAuthToken is a data type that represents an End User via either an access
71 | or request token."""
72 | # access tokens and request tokens
73 | key = None
74 | secret = None
75 |
76 | '''
77 | key = the token
78 | secret = the token secret
79 | '''
80 |
81 | def __init__(self, key, secret):
82 | self.key = key
83 | self.secret = secret
84 |
85 | def to_string(self):
86 | return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
87 |
88 | # return a token from something like:
89 | # oauth_token_secret=digg&oauth_token=digg
90 | def from_string(s):
91 | params = cgi.parse_qs(s, keep_blank_values=False)
92 | key = params['oauth_token'][0]
93 | secret = params['oauth_token_secret'][0]
94 | return OAuthToken(key, secret)
95 | from_string = staticmethod(from_string)
96 |
97 | def __str__(self):
98 | return self.to_string()
99 |
100 |
101 | class OAuthRequest(object):
102 | '''
103 | OAuthRequest represents the request and can be serialized
104 | OAuth parameters:
105 | - oauth_consumer_key
106 | - oauth_token
107 | - oauth_signature_method
108 | - oauth_signature
109 | - oauth_timestamp
110 | - oauth_nonce
111 | - oauth_version
112 | ... any additional parameters, as defined by the Service Provider.
113 | '''
114 | parameters = None # oauth parameters
115 | http_method = HTTP_METHOD
116 | http_url = None
117 | version = VERSION
118 |
119 | def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
120 | self.http_method = http_method
121 | self.http_url = http_url
122 | self.parameters = parameters or {}
123 |
124 | def set_parameter(self, parameter, value):
125 | self.parameters[parameter] = value
126 |
127 | def get_parameter(self, parameter):
128 | try:
129 | return self.parameters[parameter]
130 | except:
131 | raise OAuthError('Parameter not found: %s' % parameter)
132 |
133 | def _get_timestamp_nonce(self):
134 | return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
135 |
136 | # get any non-oauth parameters
137 | def get_nonoauth_parameters(self):
138 | parameters = {}
139 | for k, v in self.parameters.iteritems():
140 | # ignore oauth parameters
141 | if k.find('oauth_') < 0:
142 | parameters[k] = v
143 | return parameters
144 |
145 | # serialize as a header for an HTTPAuth request
146 | def to_header(self, realm=''):
147 | auth_header = 'OAuth realm="%s"' % realm
148 | # add the oauth parameters
149 | if self.parameters:
150 | for k, v in self.parameters.iteritems():
151 | if k[:6] == 'oauth_':
152 | auth_header += ', %s="%s"' % (k, escape(str(v)))
153 | return {'Authorization': auth_header}
154 |
155 | # serialize as post data for a POST request
156 | def to_postdata(self):
157 | return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()])
158 |
159 | # serialize as a url for a GET request
160 | def to_url(self):
161 | return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
162 |
163 | # return a string that consists of all the parameters that need to be signed
164 | def get_normalized_parameters(self):
165 | params = self.parameters
166 | try:
167 | # exclude the signature if it exists
168 | del params['oauth_signature']
169 | except:
170 | pass
171 | key_values = sorted(params.items())
172 | # sort lexicographically, first after key, then after value
173 | return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values])
174 |
175 | # just uppercases the http method
176 | def get_normalized_http_method(self):
177 | return self.http_method.upper()
178 |
179 | # parses the url and rebuilds it to be scheme://host/path
180 | def get_normalized_http_url(self):
181 | parts = urlparse.urlparse(self.http_url)
182 | url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
183 | return url_string
184 |
185 | # set the signature parameter to the result of build_signature
186 | def sign_request(self, signature_method, consumer, token):
187 | # set the signature method
188 | self.set_parameter('oauth_signature_method', signature_method.get_name())
189 | # set the signature
190 | self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
191 |
192 | def build_signature(self, signature_method, consumer, token):
193 | # call the build signature method within the signature method
194 | return signature_method.build_signature(self, consumer, token)
195 |
196 | def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
197 | # combine multiple parameter sources
198 | if parameters is None:
199 | parameters = {}
200 |
201 | # headers
202 | if headers and 'Authorization' in headers:
203 | auth_header = headers['Authorization']
204 | # check that the authorization header is OAuth
205 | if auth_header.index('OAuth') > -1:
206 | try:
207 | # get the parameters from the header
208 | header_params = OAuthRequest._split_header(auth_header)
209 | parameters.update(header_params)
210 | except:
211 | raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
212 |
213 | # GET or POST query string
214 | if query_string:
215 | query_params = OAuthRequest._split_url_string(query_string)
216 | parameters.update(query_params)
217 |
218 | # URL parameters
219 | param_str = urlparse.urlparse(http_url)[4] # query
220 | url_params = OAuthRequest._split_url_string(param_str)
221 | parameters.update(url_params)
222 |
223 | if parameters:
224 | return OAuthRequest(http_method, http_url, parameters)
225 |
226 | return None
227 | from_request = staticmethod(from_request)
228 |
229 | def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
230 | if not parameters:
231 | parameters = {}
232 |
233 | defaults = {
234 | 'oauth_consumer_key': oauth_consumer.key,
235 | 'oauth_timestamp': generate_timestamp(),
236 | 'oauth_nonce': generate_nonce(),
237 | 'oauth_version': OAuthRequest.version,
238 | }
239 |
240 | defaults.update(parameters)
241 | parameters = defaults
242 |
243 | if token:
244 | parameters['oauth_token'] = token.key
245 |
246 | return OAuthRequest(http_method, http_url, parameters)
247 | from_consumer_and_token = staticmethod(from_consumer_and_token)
248 |
249 | def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
250 | if not parameters:
251 | parameters = {}
252 |
253 | parameters['oauth_token'] = token.key
254 |
255 | if callback:
256 | parameters['oauth_callback'] = callback
257 |
258 | return OAuthRequest(http_method, http_url, parameters)
259 | from_token_and_callback = staticmethod(from_token_and_callback)
260 |
261 | # util function: turn Authorization: header into parameters, has to do some unescaping
262 | def _split_header(header):
263 | params = {}
264 | parts = header.split(',')
265 | for param in parts:
266 | # ignore realm parameter
267 | if param.find('OAuth realm') > -1:
268 | continue
269 | # remove whitespace
270 | param = param.strip()
271 | # split key-value
272 | param_parts = param.split('=', 1)
273 | # remove quotes and unescape the value
274 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
275 | return params
276 | _split_header = staticmethod(_split_header)
277 |
278 | # util function: turn url string into parameters, has to do some unescaping
279 | def _split_url_string(param_str):
280 | parameters = cgi.parse_qs(param_str, keep_blank_values=False)
281 | for k, v in parameters.iteritems():
282 | parameters[k] = urllib.unquote(v[0])
283 | return parameters
284 | _split_url_string = staticmethod(_split_url_string)
285 |
286 |
287 | class OAuthServer(object):
288 | """OAuthServer is a worker to check a requests validity against a data store"""
289 | timestamp_threshold = 300 # in seconds, five minutes
290 | version = VERSION
291 | signature_methods = None
292 | data_store = None
293 |
294 | def __init__(self, data_store=None, signature_methods=None):
295 | self.data_store = data_store
296 | self.signature_methods = signature_methods or {}
297 |
298 | def set_data_store(self, oauth_data_store):
299 | self.data_store = data_store
300 |
301 | def get_data_store(self):
302 | return self.data_store
303 |
304 | def add_signature_method(self, signature_method):
305 | self.signature_methods[signature_method.get_name()] = signature_method
306 | return self.signature_methods
307 |
308 | # process a request_token request
309 | # returns the request token on success
310 | def fetch_request_token(self, oauth_request):
311 | try:
312 | # get the request token for authorization
313 | token = self._get_token(oauth_request, 'request')
314 | except OAuthError:
315 | # no token required for the initial token request
316 | version = self._get_version(oauth_request)
317 | consumer = self._get_consumer(oauth_request)
318 | self._check_signature(oauth_request, consumer, None)
319 | # fetch a new token
320 | token = self.data_store.fetch_request_token(consumer)
321 | return token
322 |
323 | # process an access_token request
324 | # returns the access token on success
325 | def fetch_access_token(self, oauth_request):
326 | version = self._get_version(oauth_request)
327 | consumer = self._get_consumer(oauth_request)
328 | # get the request token
329 | token = self._get_token(oauth_request, 'request')
330 | self._check_signature(oauth_request, consumer, token)
331 | new_token = self.data_store.fetch_access_token(consumer, token)
332 | return new_token
333 |
334 | # verify an api call, checks all the parameters
335 | def verify_request(self, oauth_request):
336 | # -> consumer and token
337 | version = self._get_version(oauth_request)
338 | consumer = self._get_consumer(oauth_request)
339 | # get the access token
340 | token = self._get_token(oauth_request, 'access')
341 | self._check_signature(oauth_request, consumer, token)
342 | parameters = oauth_request.get_nonoauth_parameters()
343 | return consumer, token, parameters
344 |
345 | # authorize a request token
346 | def authorize_token(self, token, user):
347 | return self.data_store.authorize_request_token(token, user)
348 |
349 | # get the callback url
350 | def get_callback(self, oauth_request):
351 | return oauth_request.get_parameter('oauth_callback')
352 |
353 | # optional support for the authenticate header
354 | def build_authenticate_header(self, realm=''):
355 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
356 |
357 | # verify the correct version request for this server
358 | def _get_version(self, oauth_request):
359 | try:
360 | version = oauth_request.get_parameter('oauth_version')
361 | except:
362 | version = VERSION
363 | if version and version != self.version:
364 | raise OAuthError('OAuth version %s not supported.' % str(version))
365 | return version
366 |
367 | # figure out the signature with some defaults
368 | def _get_signature_method(self, oauth_request):
369 | try:
370 | signature_method = oauth_request.get_parameter('oauth_signature_method')
371 | except:
372 | signature_method = SIGNATURE_METHOD
373 | try:
374 | # get the signature method object
375 | signature_method = self.signature_methods[signature_method]
376 | except:
377 | signature_method_names = ', '.join(self.signature_methods.keys())
378 | raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
379 |
380 | return signature_method
381 |
382 | def _get_consumer(self, oauth_request):
383 | consumer_key = oauth_request.get_parameter('oauth_consumer_key')
384 | if not consumer_key:
385 | raise OAuthError('Invalid consumer key.')
386 | consumer = self.data_store.lookup_consumer(consumer_key)
387 | if not consumer:
388 | raise OAuthError('Invalid consumer.')
389 | return consumer
390 |
391 | # try to find the token for the provided request token key
392 | def _get_token(self, oauth_request, token_type='access'):
393 | token_field = oauth_request.get_parameter('oauth_token')
394 | token = self.data_store.lookup_token(token_type, token_field)
395 | if not token:
396 | raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
397 | return token
398 |
399 | def _check_signature(self, oauth_request, consumer, token):
400 | timestamp, nonce = oauth_request._get_timestamp_nonce()
401 | self._check_timestamp(timestamp)
402 | self._check_nonce(consumer, token, nonce)
403 | signature_method = self._get_signature_method(oauth_request)
404 | try:
405 | signature = oauth_request.get_parameter('oauth_signature')
406 | except:
407 | raise OAuthError('Missing signature.')
408 | # validate the signature
409 | valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
410 | if not valid_sig:
411 | key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
412 | raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
413 | built = signature_method.build_signature(oauth_request, consumer, token)
414 |
415 | def _check_timestamp(self, timestamp):
416 | # verify that timestamp is recentish
417 | timestamp = int(timestamp)
418 | now = int(time.time())
419 | lapsed = now - timestamp
420 | if lapsed > self.timestamp_threshold:
421 | raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
422 |
423 | def _check_nonce(self, consumer, token, nonce):
424 | # verify that the nonce is uniqueish
425 | nonce = self.data_store.lookup_nonce(consumer, token, nonce)
426 | if nonce:
427 | raise OAuthError('Nonce already used: %s' % str(nonce))
428 |
429 |
430 | class OAuthClient(object):
431 | """OAuthClient is a worker to attempt to execute a request."""
432 | consumer = None
433 | token = None
434 |
435 | def __init__(self, oauth_consumer, oauth_token):
436 | self.consumer = oauth_consumer
437 | self.token = oauth_token
438 |
439 | def get_consumer(self):
440 | return self.consumer
441 |
442 | def get_token(self):
443 | return self.token
444 |
445 | def fetch_request_token(self, oauth_request):
446 | # -> OAuthToken
447 | raise NotImplementedError
448 |
449 | def fetch_access_token(self, oauth_request):
450 | # -> OAuthToken
451 | raise NotImplementedError
452 |
453 | def access_resource(self, oauth_request):
454 | # -> some protected resource
455 | raise NotImplementedError
456 |
457 |
458 | class OAuthDataStore(object):
459 | """OAuthDataStore is a database abstraction used to lookup consumers and tokens"""
460 |
461 | def lookup_consumer(self, key):
462 | # -> OAuthConsumer
463 | raise NotImplementedError
464 |
465 | def lookup_token(self, oauth_consumer, token_type, token_token):
466 | # -> OAuthToken
467 | raise NotImplementedError
468 |
469 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
470 | # -> OAuthToken
471 | raise NotImplementedError
472 |
473 | def fetch_request_token(self, oauth_consumer):
474 | # -> OAuthToken
475 | raise NotImplementedError
476 |
477 | def fetch_access_token(self, oauth_consumer, oauth_token):
478 | # -> OAuthToken
479 | raise NotImplementedError
480 |
481 | def authorize_request_token(self, oauth_token, user):
482 | # -> OAuthToken
483 | raise NotImplementedError
484 |
485 |
486 | class OAuthSignatureMethod(object):
487 | """OAuthSignatureMethod is a strategy class that implements a signature method"""
488 |
489 | def get_name(self):
490 | # -> str
491 | raise NotImplementedError
492 |
493 | def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
494 | # -> str key, str raw
495 | raise NotImplementedError
496 |
497 | def build_signature(self, oauth_request, oauth_consumer, oauth_token):
498 | # -> str
499 | raise NotImplementedError
500 |
501 | def check_signature(self, oauth_request, consumer, token, signature):
502 | built = self.build_signature(oauth_request, consumer, token)
503 | return built == signature
504 |
505 |
506 | class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
507 |
508 | def get_name(self):
509 | return 'HMAC-SHA1'
510 |
511 | def build_signature_base_string(self, oauth_request, consumer, token):
512 | sig = (
513 | escape(oauth_request.get_normalized_http_method()),
514 | escape(oauth_request.get_normalized_http_url()),
515 | escape(oauth_request.get_normalized_parameters()),
516 | )
517 |
518 | key = '%s&' % escape(consumer.secret)
519 | if token:
520 | key += escape(token.secret)
521 | raw = '&'.join(sig)
522 | return key, raw
523 |
524 | def build_signature(self, oauth_request, consumer, token):
525 | # build the base signature string
526 | key, raw = self.build_signature_base_string(oauth_request, consumer, token)
527 |
528 | # hmac object
529 | try:
530 | import hashlib # 2.5
531 | hashed = hmac.new(key, raw, hashlib.sha1)
532 | except:
533 | import sha # deprecated
534 | hashed = hmac.new(key, raw, sha)
535 |
536 | # calculate the digest base 64
537 | return binascii.b2a_base64(hashed.digest())[:-1]
538 |
539 |
540 | class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
541 |
542 | def get_name(self):
543 | return 'PLAINTEXT'
544 |
545 | def build_signature_base_string(self, oauth_request, consumer, token):
546 | # concatenate the consumer key and secret
547 | sig = escape(consumer.secret) + '&'
548 | if token:
549 | sig = sig + escape(token.secret)
550 | return sig
551 |
552 | def build_signature(self, oauth_request, consumer, token):
553 | return self.build_signature_base_string(oauth_request, consumer, token)
554 |
--------------------------------------------------------------------------------
/googleappsauth/openid.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/tools.py -
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009 HUDORA GmbH. All rights reserved.
8 | """
9 |
10 | import re
11 | import urllib
12 |
13 |
14 | class OpenIdError(Exception):
15 |
16 | def __init__(self, why=None):
17 | Exception.__init__(self, why)
18 | self.why = why
19 |
20 |
21 | def build_login_url(endpoint_url, realm, callback_url, oauth_consumer=None, oauth_scope=None):
22 | # zuerst ueberpruefen wir, ob die Callback Url gueltig ist
23 | if not endpoint_url:
24 | raise OpenIdError('invalid GOOGLE_OPENID_ENDPOINT %r' % endpoint_url)
25 | if not realm:
26 | raise OpenIdError('invalid GOOGLE_OPENID_REALM %r' % realm)
27 | if not callback_url:
28 | raise OpenIdError('invalid callback url %r' % callback_url)
29 |
30 | # 'openid.mode': 'checkid_setup' oder 'checkid_immediate'
31 | params = {
32 | # zuerst die Keys fuer die eigentliche Authentifizierung
33 | 'openid.ns': 'http://specs.openid.net/auth/2.0',
34 | 'openid.mode': 'checkid_setup',
35 | 'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
36 | 'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
37 | 'openid.realm': realm,
38 | 'openid.return_to': callback_url,
39 |
40 | # jetzt noch die Keys fuer die 'extended attributes', damit wir den
41 | # Realnamen und die Emailadresse des eingeloggten Benutzers bekommen
42 | 'openid.ns.ax': 'http://openid.net/srv/ax/1.0',
43 | 'openid.ax.mode': 'fetch_request',
44 | 'openid.ax.required': 'firstname,lastname,language,email',
45 | 'openid.ax.type.email': 'http://axschema.org/contact/email',
46 | 'openid.ax.type.firstname': 'http://axschema.org/namePerson/first',
47 | 'openid.ax.type.language': 'http://axschema.org/pref/language',
48 | 'openid.ax.type.lastname': 'http://axschema.org/namePerson/last',
49 | }
50 |
51 | # und schliesslich noch die Keys fuer OAuth, damit wir einen
52 | # Request Key bekommen, den wir dann auf Wunsch zum Access Key
53 | # machen koennen (notwendig fuer einen API-Zugriff auf GApps)
54 | if oauth_consumer and oauth_scope:
55 | params['openid.ns.oauth']='http://specs.openid.net/extensions/oauth/1.0'
56 | params['openid.oauth.consumer']=oauth_consumer
57 | params['openid.oauth.scope']=oauth_scope
58 |
59 | # jetzt bauen wir die Parameter zusammen mit der URL des OpenID-
60 | # Endpoints noch zu einer kompletten URL zusammen und liefern
61 | # diese zurueck
62 | urlencoded_params = urllib.urlencode(params)
63 | redirect_url = endpoint_url
64 | if endpoint_url.find('?') == -1:
65 | redirect_url += '?%s' % urlencoded_params
66 | else:
67 | redirect_url += '&%s' % urlencoded_params
68 | return redirect_url
69 |
70 |
71 | def parse_login_response(request, callback_url=None):
72 | # haben wir ueberhaupt eine positive Antwort?
73 | args = _get_request_args(request)
74 | is_valid_logon = args.get('openid.mode') == 'id_res'
75 |
76 | # basic checks: stimmen die URLs ueberein?
77 | if callback_url:
78 | if callback_url != _lookup_key(args, 'openid.return_to'):
79 | is_valid_logon = None
80 |
81 | # wir holen uns den OpenID identifier
82 | identifier = _lookup_key(args, 'openid.identity')
83 | if identifier == None:
84 | identifier = _lookup_key(args, 'openid.claimed_id')
85 |
86 | # wenn der Login gueltig war liefern wir jetzt den
87 | # OpenID-Identifier zurueck, ansonsten None
88 | if is_valid_logon:
89 | return identifier
90 | else:
91 | return None
92 |
93 |
94 | def get_email(request):
95 | return _lookup_key(_get_request_args(request), 'value.email')
96 |
97 |
98 | def get_language(request):
99 | return _lookup_key(_get_request_args(request), 'value.language')
100 |
101 |
102 | def get_firstname(request):
103 | return _lookup_key(_get_request_args(request), 'value.firstname')
104 |
105 |
106 | def get_lastname(request):
107 | return _lookup_key(_get_request_args(request), 'value.lastname')
108 |
109 |
110 | def get_oauth_request_token(request):
111 | return _lookup_key(_get_request_args(request), 'request_token')
112 |
113 |
114 | def _get_request_args(request):
115 | args = request.GET
116 | if request.method == 'POST':
117 | args = request.POST
118 | return args
119 |
120 |
121 | def _lookup_key(args, key_pattern):
122 | for key, value in args.items():
123 | if key == key_pattern or re.search(key_pattern, key):
124 | if isinstance(value, list):
125 | return value[0]
126 | else:
127 | return value
128 | return None
129 |
--------------------------------------------------------------------------------
/googleappsauth/templates/googleappsauth/domains.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Bitte Google-Login-Domain auswaehlen!
5 |
6 |
30 |
31 |
32 |
33 |
Bitte wählen Sie die Domain aus,
über die Sie sich einloggen möchten:
34 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/googleappsauth/utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleauth/tools.py -
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009 HUDORA GmbH. All rights reserved.
8 | """
9 |
10 | import oauth
11 | import httplib
12 | import random
13 | from django.conf import settings
14 |
15 |
16 | """ Google OAuth Key und Secret, wird im Backend fuer hudora.de konfiguriert """
17 | _apps_domain = getattr(settings, 'GOOGLE_APPS_DOMAIN', None)
18 | _consumer_key = getattr(settings, 'GOOGLE_APPS_CONSUMER_KEY', None)
19 | _consumer_secret = getattr(settings, 'GOOGLE_APPS_CONSUMER_SECRET', None)
20 |
21 |
22 | """ Google OAuth URLs, auf die zugegriffen werden soll """
23 | SERVER = 'www.google.com'
24 | REQUEST_TOKEN_URL = 'https://%s/accounts/OAuthGetRequestToken' % SERVER
25 | AUTHORIZATION_URL = 'https://%s/accounts/OAuthAuthorizeToken' % SERVER
26 | ACCESS_TOKEN_URL = 'https://%s/accounts/OAuthGetAccessToken' % SERVER
27 | PROFILES_URL = 'http://%s/m8/feeds/profiles/domain/%s/full/' % (SERVER, _apps_domain)
28 |
29 |
30 | """ die globalen Objekte zum Zugriff auf Google OAuth """
31 | _consumer = oauth.OAuthConsumer(_consumer_key, _consumer_secret)
32 | _connection = httplib.HTTPSConnection(SERVER)
33 | _signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
34 |
35 |
36 | def fetch_response(req, conn):
37 | """
38 | helper method, fuehrt einen HTTP-Request durch und liefert die
39 | vom Server gelieferte Antwort als String zurueck.
40 | """
41 | conn.request(req.http_method, req.to_url())
42 | resp = conn.getresponse()
43 | return resp.read()
44 |
45 |
46 | def token_from_session(request, attribute_name='access_token'):
47 | """
48 | helper method, liesst das serialisierte Access Token aus der
49 | Session und erzeugt wieder ein Object daraus.
50 | """
51 | token_str = request.session.get(attribute_name, None)
52 | if not token_str:
53 | return None
54 | return token_from_string(token_str)
55 |
56 |
57 | def token_from_string(serialized_token):
58 | """
59 | helper method, konvertiert ein als String serialisiertes
60 | Token wieder zurueck in ein Python Object
61 | """
62 | token = oauth.OAuthToken.from_string(serialized_token)
63 | return token
64 |
65 |
66 | def get_request_token(callback_url, google_scope):
67 | """
68 | OAuth call, laedt ein neuen Request-Token vom Server
69 | """
70 | req = oauth.OAuthRequest.from_consumer_and_token(_consumer,
71 | http_url=REQUEST_TOKEN_URL,
72 | parameters={'scope': google_scope,
73 | 'oauth_callback': callback_url})
74 | req.sign_request(_signature_method, _consumer, None)
75 | resp = fetch_response(req, _connection)
76 | req_token = oauth.OAuthToken.from_string(resp)
77 | return req_token
78 |
79 |
80 | def get_access_token(req_token, verifier=None):
81 | """
82 | OAuth call, laedt nach erfolgtem Auth des Users und
83 | der App das eigentliche Access-Token von Google. Mit diesem
84 | Token koennen dann die Calls durchgefuehrt werden, fuer die
85 | bei Google ein vorheriges Auth notwendig ist.
86 | """
87 | parameters={}
88 | if verifier:
89 | parameters['oauth_verifier'] = verifier
90 |
91 | req = oauth.OAuthRequest.from_consumer_and_token(_consumer, token=req_token,
92 | http_url=ACCESS_TOKEN_URL, parameters=parameters)
93 | req.sign_request(_signature_method, _consumer, req_token)
94 | resp = fetch_response(req, _connection)
95 | access_token = oauth.OAuthToken.from_string(resp)
96 | return access_token
97 |
98 |
99 | def build_auth_url(req_token):
100 | """
101 | OAuth call, erzeugt aus dem vorher geladenen Request-Token
102 | die URL, auf die der Benutzer zu Google umgeleitet werden muss. Dort
103 | authorisiert der Benutzer dann zuerst sich selbst und in der Folge unsere
104 | App zum Zugriff auf das API. Nach erfolgtem Auth leitet Google den Benutzer
105 | auf die bei Google hinterlegte URL zurueck zur App, es muss als der
106 | richtige Key genutzt werden, damit der Redirect wirklich auf unseren
107 | Server geht.
108 | """
109 |
110 | req = oauth.OAuthRequest.from_consumer_and_token(_consumer, token=req_token,
111 | http_url=AUTHORIZATION_URL,
112 | parameters={'hd': _apps_domain})
113 | req.sign_request(_signature_method, _consumer, req_token)
114 | auth_url = req.to_url()
115 | return auth_url
116 |
117 |
118 | def get_user_profile(access_token, username):
119 | req = oauth.OAuthRequest.from_consumer_and_token(_consumer, token=access_token,
120 | http_method='GET',
121 | http_url=PROFILES_URL + username,
122 | parameters={'v': '3.0'})
123 | req.sign_request(_signature_method, _consumer, access_token)
124 | resp = fetch_response(req, _connection)
125 | return 'schluete'
126 |
127 |
128 |
129 | # OpenID
130 | # https://www.google.com/accounts/o8/site-xrds?hd=hudora.de
131 | # user's login identifier, as openid.claimed_id
132 | # requested user attributes, as openid.ax.value.email (if requested)
133 | # authorized OAuth request token, as openid.ext2.request_token (if requested)
134 |
--------------------------------------------------------------------------------
/googleappsauth/views.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | """
4 | googleappsauth/views.py -
5 |
6 | Created by Axel Schlüter on 2009-12
7 | Copyright (c) 2009, 2010 HUDORA GmbH. All rights reserved.
8 | """
9 |
10 | import types
11 |
12 | from django.conf import settings
13 | from django.core.urlresolvers import reverse
14 | from django.contrib.auth import REDIRECT_FIELD_NAME
15 | from django.http import HttpResponse, HttpResponseRedirect, Http404
16 | from django.shortcuts import render_to_response
17 | import django.contrib.auth as djauth
18 | import googleappsauth.openid
19 |
20 |
21 | _google_apps_domain = getattr(settings, 'GOOGLE_APPS_DOMAIN', None)
22 | _google_openid_endpoint = getattr(settings, 'GOOGLE_OPENID_ENDPOINT', None)
23 | _google_openid_realm = getattr(settings, 'GOOGLE_OPENID_REALM', None)
24 | _oauth_consumer_key = getattr(settings, 'GOOGLE_APPS_CONSUMER_KEY', None)
25 | _oauth_consumer_secret = getattr(settings, 'GOOGLE_APPS_CONSUMER_SECRET', None)
26 | _google_api_scope = getattr(settings, 'GOOGLE_API_SCOPE', None)
27 |
28 | _login_url = getattr(settings, 'LOGIN_URL', None)
29 |
30 |
31 | def login(request, redirect_field_name=REDIRECT_FIELD_NAME, redirect_url=None):
32 | # wenn wir ueber einen Post-Request in die Method gekommen sind gehen
33 | # wir davon aus, das der Benutzer vorher eine Domain fuer den Login
34 | # ausgewaehlt hat. Ansonsten ist's ein Fehler.
35 | if request.method == 'POST':
36 | callback_url = request.session['callback_url']
37 | login_domain = request.POST.get('domain')
38 | if not login_domain:
39 | raise Http404('invalid or missing login domain!')
40 |
41 | # ansonsten ist das ein Login-Versuch, also bestimmen wir zuerst, wohin
42 | # nach erfolgtem Login in die App umgeleitet werden soll
43 | else:
44 | login_domain = None
45 | if not redirect_url:
46 | redirect_url = request.REQUEST.get(redirect_field_name)
47 | if not redirect_url:
48 | redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/')
49 | request.session['redirect_url'] = redirect_url
50 |
51 | # jetzt bauen wir uns die URL fuer den Callback zusammen, unter
52 | # dem wir von Google aufgerufen werden moechten nach dem Login
53 | callback_url = request.build_absolute_uri(reverse(callback))
54 | request.session['callback_url'] = callback_url
55 |
56 | # wenn wir mehr als eine Apps-Domain konfiguriert haben und noch
57 | # keine Login-Domain aus dem POST-Request ausgewaehlt wurde dann
58 | # dann zeigen wir jetzt zuerst noch eine Auswahlbox fuer die
59 | # gewuenschte Login-Domain an.
60 | if not login_domain:
61 | if type(_google_apps_domain) == types.ListType:
62 | return render_to_response('googleappsauth/domains.html',
63 | { 'login_url': _login_url, 'domains': _google_apps_domain })
64 | else:
65 | login_domain = _google_apps_domain
66 |
67 | # jetzt haben wir ganz sicher eine Domain, ueber die wir uns einloggen
68 | # sollen. Um die Kompatibilitaet mit alten Versionen (in denen der Settings-
69 | # Parameter 'GOOGLE_OPENID_ENDPOINT' bereits die vollstaendige Endpoint-URL
70 | # inkl. Login-Domain enthalten hat) nicht zu brechen fangen wir hier moegliche
71 | # Typfehler (eben wenn der Parameter kein passendes '%s' enthaelt) ab.
72 | openid_endpoint = _google_openid_endpoint
73 | try:
74 | openid_endpoint = openid_endpoint % login_domain
75 | except TypeError:
76 | pass
77 |
78 | # und schliesslich konstruieren wir darauf die Google-OpenID-
79 | # Endpoint-URL, auf die wir dann den Benutzer umleiten
80 | url = googleappsauth.openid.build_login_url(
81 | openid_endpoint, _google_openid_realm,
82 | callback_url, _oauth_consumer_key, _google_api_scope)
83 | return HttpResponseRedirect(url)
84 |
85 |
86 | def callback(request):
87 | # haben wir einen erfolgreichen Login? Wenn nicht gehen wir
88 | # sofort zurueck, ohne einen Benutzer einzuloggen
89 | callback_url = request.session.get('callback_url', '/')
90 | identifier = googleappsauth.openid.parse_login_response(request, callback_url)
91 | if not identifier:
92 | # TODO: was ist hier los?
93 | return HttpResponseRedirect('/')
94 |
95 | # jetzt holen wir uns die restlichen Daten aus dem Login
96 | attributes = {
97 | 'email': googleappsauth.openid.get_email(request),
98 | 'language': googleappsauth.openid.get_language(request),
99 | 'firstname': googleappsauth.openid.get_firstname(request),
100 | 'lastname': googleappsauth.openid.get_lastname(request)}
101 |
102 | # wenn wir ein OAuth request token bekommen haben machen wir
103 | # daraus jetzt noch flott ein access token
104 | request_token = googleappsauth.openid.get_oauth_request_token(request)
105 | #if request_token:
106 | # attributes['access_token'] = None
107 | # raise Exception('access token handling not yet implemented!')
108 |
109 | # Usernames are based on E-Mail Addresses which are unique.
110 | username = attributes.get('email', identifier).split('@')[0].replace('.', '')
111 |
112 | # schliesslich melden wir den Benutzer mit seinen Attributen am
113 | # Auth-System von Django an, dann zurueck zur eigentlichen App
114 | user = djauth.authenticate(identifier=username, attributes=attributes)
115 | if not user:
116 | # For some reason I do not fully understand we get back a "None"" coasionalty - retry.
117 | user = djauth.authenticate(identifier=username, attributes=attributes)
118 | if not user:
119 | # die Authentifizierung ist gescheitert
120 | raise RuntimeError("Authentifizierungsproblem: %s|%s|%s" % (username, identifier, attributes))
121 | djauth.login(request, user)
122 | redirect_url = request.session['redirect_url']
123 | # del request.session['redirect_url']
124 | return HttpResponseRedirect(redirect_url)
125 |
126 |
127 | def logout(request):
128 | djauth.logout(request)
129 | return HttpResponseRedirect('https://www.google.com/a/%s/Logout' % _google_apps_domain)
130 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | setuptools
2 | Django>=1.0.2-final
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | from setuptools import setup, find_packages
3 |
4 | setup(name='googleappsauth',
5 | maintainer='Maximillian Dornseif',
6 | maintainer_email='md@hudora.de',
7 | version='1.1',
8 | description='googleappsauth authenticates Django Users against a Google Apps Domain',
9 | long_description=codecs.open('README.rst', "r", "utf-8").read(),
10 | license='BSD',
11 | url='http://github.com/hudora/django-googleappsauth#readme',
12 | classifiers=['Intended Audience :: Developers',
13 | 'Programming Language :: Python'],
14 | package_data={"googleappsauth": ["templates/googleappsauth/*.html",]},
15 | packages = find_packages(),
16 | install_requires = ['Django'],
17 | zip_safe = False,
18 | )
19 |
--------------------------------------------------------------------------------