├── .gitignore ├── AUTHORS ├── CHANGES ├── MANIFEST.in ├── Makefile ├── README.rst ├── googleappsauth ├── __init__.py ├── backends.py ├── middleware.py ├── oauth.py ├── openid.py ├── templates │ └── googleappsauth │ │ └── domains.html ├── utils.py └── views.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.bak -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Axel Schlueter 2 | Francis Gulotta 3 | Maximillian Dornseif 4 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.03 AUTH_PROTECTED_AREAS now is a list (legavy support in place until Version 2.0) 2 | 1.02 GOOGLE_OPENID_ENDPOINT and GOOGLE_API_SCOPE not required anymore, 3 | ignoring of access_token 4 | 1.01p4 - graceful handling of broken session engine 5 | 1.01 - tranport QUERY_STRING through google Authentication, Logout at Google 6 | 1.0 - stand alone package 7 | 0.26 - comes with the googleauth suite 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := ./testenv/bin:$(PATH) 2 | 3 | default: dependencies check test statistics 4 | 5 | check: 6 | find googleappsauth -name '*.py' | xargs /usr/local/hudorakit/bin/hd_pep8 7 | /usr/local/hudorakit/bin/hd_pylint -f parseable googleappsauth | tee .pylint.out 8 | 9 | install: build 10 | sudo python setup.py install 11 | 12 | dependencies: 13 | virtualenv testenv 14 | pip -q install -E testenv -r requirements.txt 15 | 16 | statistics: 17 | sloccount --wide --details googleappsauth | grep -E '^[0-9]' > .sloccount.sc 18 | 19 | build: 20 | python setup.py build sdist bdist_egg 21 | 22 | upload: build 23 | python setup.py sdist upload 24 | 25 | clean: 26 | rm -Rf testenv build dist html test.db .pylint.out .sloccount.sc pip-log.txt 27 | find . -name '*.pyc' -or -name '*.pyo' -delete 28 | 29 | .PHONY: test build clean install upload check 30 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ==================================================== 2 | Authentication agains Google Apps Domains for Django 3 | ==================================================== 4 | 5 | *googleappsauth* allows you to authenticate your `Django `_ users 6 | against an Google `Apps `_ domain. 7 | This means you basically get a single sign-on solution, provided that all users of your django application 8 | also have Accounts in Google Apps for your Domain. 9 | 10 | 11 | Usage 12 | ===== 13 | 14 | To use googleappsauth, configuration in `settings.py` should look like this:: 15 | 16 | GOOGLE_APPS_DOMAIN = 'example.com' 17 | GOOGLE_APPS_CONSUMER_KEY = 'example.com' 18 | GOOGLE_APPS_CONSUMER_SECRET = '*sekret*' 19 | # domain where your application is running 20 | GOOGLE_OPENID_REALM = 'http://*.hudora.biz/' 21 | 22 | If you want to choose from a list of Google apps domains on a login-by-login basis 23 | you can configure an array of apps domains:: 24 | 25 | GOOGLE_APPS_DOMAIN = ['example.com', 'foobar.com', 'carcdr.de'] 26 | 27 | You also can tell googleappsauth where to go after successfull authentication, in case 28 | the redirect_url had not been set. `LOGIN_REDIRECT_URL` defaults to `/`. 29 | :: 30 | 31 | LOGIN_REDIRECT_URL = '/admin' 32 | 33 | To activate googleappsauth, set the appropriate Authentication backend and include a callback view. 34 | :: 35 | 36 | settings.py: 37 | AUTHENTICATION_BACKENDS = ('googleappsauth.backends.GoogleAuthBackend',) 38 | 39 | urls.py: 40 | (r'^callback_googleappsauth/', 'googleappsauth.views.callback'), 41 | 42 | 43 | Using a special middleware which is included in the package, you can block access to a compete site. 44 | :: 45 | 46 | MIDDLEWARE_CLASSES = ( 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'googleappsauth.middleware.GoogleAuthMiddleware', 51 | ) 52 | 53 | In addition you can set `AUTH_PROTECTED_AREAS` to authenticate only access to certain parts of a site, e.g. 54 | :: 55 | 56 | AUTH_PROTECTED_AREAS = ['/admin'] 57 | 58 | If you wish, you can add `AUTH_EXCLUDED_AREAS` to remove authentication from sub-folders of your protected areas, e.g. 59 | :: 60 | 61 | AUTH_EXCLUDED_AREAS = ['/admin/public'] 62 | 63 | Download 64 | ======== 65 | 66 | Get it at the `Python Cheeseshop `_ or at 67 | `GitHub `_. 68 | 69 | See also 70 | ======== 71 | 72 | * Tim Garthwaite: `Google Apps Auth Backend for Django `_ 73 | * http://github.com/flashingpumpkin/django-socialregistration/ 74 | * http://github.com/uswaretech/Django-Socialauth/ 75 | * http://bitbucket.org/benoitc/django-authopenid/ 76 | * http://djangosnippets.org/snippets/913/ 77 | * http://pkropf.blogspot.com/2009/05/django-google-apps-authentication.html 78 | * http://github.com/adieu/python-openid/ - patched for Google Apps 79 | 80 | Notes 81 | ----- 82 | 83 | You might need to enable the OAuth+OpenID support in your Google Apps domain 84 | at http://www.google.com/a/cpanel//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 |
35 | 40 |
41 | 42 |
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 | --------------------------------------------------------------------------------