1# Copyright 2015 Google Inc. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Utilities for the Django web framework. 16 17Provides Django views and helpers the make using the OAuth2 web server 18flow easier. It includes an ``oauth_required`` decorator to automatically 19ensure that user credentials are available, and an ``oauth_enabled`` decorator 20to check if the user has authorized, and helper shortcuts to create the 21authorization URL otherwise. 22 23There are two basic use cases supported. The first is using Google OAuth as the 24primary form of authentication, which is the simpler approach recommended 25for applications without their own user system. 26 27The second use case is adding Google OAuth credentials to an 28existing Django model containing a Django user field. Most of the 29configuration is the same, except for `GOOGLE_OAUTH_MODEL_STORAGE` in 30settings.py. See "Adding Credentials To An Existing Django User System" for 31usage differences. 32 33Only Django versions 1.8+ are supported. 34 35Configuration 36=============== 37 38To configure, you'll need a set of OAuth2 web application credentials from 39`Google Developer's Console <https://console.developers.google.com/project/_/apiui/credential>`. 40 41Add the helper to your INSTALLED_APPS: 42 43.. code-block:: python 44 :caption: settings.py 45 :name: installed_apps 46 47 INSTALLED_APPS = ( 48 # other apps 49 "django.contrib.sessions.middleware" 50 "oauth2client.contrib.django_util" 51 ) 52 53This helper also requires the Django Session Middleware, so 54``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well. 55 56Add the client secrets created earlier to the settings. You can either 57specify the path to the credentials file in JSON format 58 59.. code-block:: python 60 :caption: settings.py 61 :name: secrets_file 62 63 GOOGLE_OAUTH2_CLIENT_SECRETS_JSON=/path/to/client-secret.json 64 65Or, directly configure the client Id and client secret. 66 67 68.. code-block:: python 69 :caption: settings.py 70 :name: secrets_config 71 72 GOOGLE_OAUTH2_CLIENT_ID=client-id-field 73 GOOGLE_OAUTH2_CLIENT_SECRET=client-secret-field 74 75By default, the default scopes for the required decorator only contains the 76``email`` scopes. You can change that default in the settings. 77 78.. code-block:: python 79 :caption: settings.py 80 :name: scopes 81 82 GOOGLE_OAUTH2_SCOPES = ('email', 'https://www.googleapis.com/auth/calendar',) 83 84By default, the decorators will add an `oauth` object to the Django request 85object, and include all of its state and helpers inside that object. If the 86`oauth` name conflicts with another usage, it can be changed 87 88.. code-block:: python 89 :caption: settings.py 90 :name: request_prefix 91 92 # changes request.oauth to request.google_oauth 93 GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'google_oauth' 94 95Add the oauth2 routes to your application's urls.py urlpatterns. 96 97.. code-block:: python 98 :caption: urls.py 99 :name: urls 100 101 from oauth2client.contrib.django_util.site import urls as oauth2_urls 102 103 urlpatterns += [url(r'^oauth2/', include(oauth2_urls))] 104 105To require OAuth2 credentials for a view, use the `oauth2_required` decorator. 106This creates a credentials object with an id_token, and allows you to create 107an `http` object to build service clients with. These are all attached to the 108request.oauth 109 110.. code-block:: python 111 :caption: views.py 112 :name: views_required 113 114 from oauth2client.contrib.django_util.decorators import oauth_required 115 116 @oauth_required 117 def requires_default_scopes(request): 118 email = request.oauth.credentials.id_token['email'] 119 service = build(serviceName='calendar', version='v3', 120 http=request.oauth.http, 121 developerKey=API_KEY) 122 events = service.events().list(calendarId='primary').execute()['items'] 123 return HttpResponse("email: {0} , calendar: {1}".format( 124 email,str(events))) 125 return HttpResponse( 126 "email: {0} , calendar: {1}".format(email, str(events))) 127 128To make OAuth2 optional and provide an authorization link in your own views. 129 130.. code-block:: python 131 :caption: views.py 132 :name: views_enabled2 133 134 from oauth2client.contrib.django_util.decorators import oauth_enabled 135 136 @oauth_enabled 137 def optional_oauth2(request): 138 if request.oauth.has_credentials(): 139 # this could be passed into a view 140 # request.oauth.http is also initialized 141 return HttpResponse("User email: {0}".format( 142 request.oauth.credentials.id_token['email'])) 143 else: 144 return HttpResponse( 145 'Here is an OAuth Authorize link: <a href="{0}">Authorize' 146 '</a>'.format(request.oauth.get_authorize_redirect())) 147 148If a view needs a scope not included in the default scopes specified in 149the settings, you can use [incremental auth](https://developers.google.com/identity/sign-in/web/incremental-auth) 150and specify additional scopes in the decorator arguments. 151 152.. code-block:: python 153 :caption: views.py 154 :name: views_required_additional_scopes 155 156 @oauth_enabled(scopes=['https://www.googleapis.com/auth/drive']) 157 def drive_required(request): 158 if request.oauth.has_credentials(): 159 service = build(serviceName='drive', version='v2', 160 http=request.oauth.http, 161 developerKey=API_KEY) 162 events = service.files().list().execute()['items'] 163 return HttpResponse(str(events)) 164 else: 165 return HttpResponse( 166 'Here is an OAuth Authorize link: <a href="{0}">Authorize' 167 '</a>'.format(request.oauth.get_authorize_redirect())) 168 169 170To provide a callback on authorization being completed, use the 171oauth2_authorized signal: 172 173.. code-block:: python 174 :caption: views.py 175 :name: signals 176 177 from oauth2client.contrib.django_util.signals import oauth2_authorized 178 179 def test_callback(sender, request, credentials, **kwargs): 180 print("Authorization Signal Received {0}".format( 181 credentials.id_token['email'])) 182 183 oauth2_authorized.connect(test_callback) 184 185Adding Credentials To An Existing Django User System 186===================================================== 187 188As an alternative to storing the credentials in the session, the helper 189can be configured to store the fields on a Django model. This might be useful 190if you need to use the credentials outside the context of a user request. It 191also prevents the need for a logged in user to repeat the OAuth flow when 192starting a new session. 193 194To use, change ``settings.py`` 195 196.. code-block:: python 197 :caption: settings.py 198 :name: storage_model_config 199 200 GOOGLE_OAUTH2_STORAGE_MODEL = { 201 'model': 'path.to.model.MyModel', 202 'user_property': 'user_id', 203 'credentials_property': 'credential' 204 } 205 206Where ``path.to.model`` class is the fully qualified name of a 207``django.db.model`` class containing a ``django.contrib.auth.models.User`` 208field with the name specified by `user_property` and a 209:class:`oauth2client.contrib.django_util.models.CredentialsField` with the name 210specified by `credentials_property`. For the sample configuration given, 211our model would look like 212 213.. code-block:: python 214 :caption: models.py 215 :name: storage_model_model 216 217 from django.contrib.auth.models import User 218 from oauth2client.contrib.django_util.models import CredentialsField 219 220 class MyModel(models.Model): 221 # ... other fields here ... 222 user = models.OneToOneField(User) 223 credential = CredentialsField() 224""" 225 226import importlib 227 228import django.conf 229from django.core import exceptions 230from django.core import urlresolvers 231import httplib2 232from six.moves.urllib import parse 233 234from oauth2client import clientsecrets 235from oauth2client.contrib import dictionary_storage 236from oauth2client.contrib.django_util import storage 237 238GOOGLE_OAUTH2_DEFAULT_SCOPES = ('email',) 239GOOGLE_OAUTH2_REQUEST_ATTRIBUTE = 'oauth' 240 241 242def _load_client_secrets(filename): 243 """Loads client secrets from the given filename. 244 245 Args: 246 filename: The name of the file containing the JSON secret key. 247 248 Returns: 249 A 2-tuple, the first item containing the client id, and the second 250 item containing a client secret. 251 """ 252 client_type, client_info = clientsecrets.loadfile(filename) 253 254 if client_type != clientsecrets.TYPE_WEB: 255 raise ValueError( 256 'The flow specified in {} is not supported, only the WEB flow ' 257 'type is supported.'.format(client_type)) 258 return client_info['client_id'], client_info['client_secret'] 259 260 261def _get_oauth2_client_id_and_secret(settings_instance): 262 """Initializes client id and client secret based on the settings. 263 264 Args: 265 settings_instance: An instance of ``django.conf.settings``. 266 267 Returns: 268 A 2-tuple, the first item is the client id and the second 269 item is the client secret. 270 """ 271 secret_json = getattr(settings_instance, 272 'GOOGLE_OAUTH2_CLIENT_SECRETS_JSON', None) 273 if secret_json is not None: 274 return _load_client_secrets(secret_json) 275 else: 276 client_id = getattr(settings_instance, "GOOGLE_OAUTH2_CLIENT_ID", 277 None) 278 client_secret = getattr(settings_instance, 279 "GOOGLE_OAUTH2_CLIENT_SECRET", None) 280 if client_id is not None and client_secret is not None: 281 return client_id, client_secret 282 else: 283 raise exceptions.ImproperlyConfigured( 284 "Must specify either GOOGLE_OAUTH2_CLIENT_SECRETS_JSON, or " 285 "both GOOGLE_OAUTH2_CLIENT_ID and " 286 "GOOGLE_OAUTH2_CLIENT_SECRET in settings.py") 287 288 289def _get_storage_model(): 290 """This configures whether the credentials will be stored in the session 291 or the Django ORM based on the settings. By default, the credentials 292 will be stored in the session, unless `GOOGLE_OAUTH2_STORAGE_MODEL` 293 is found in the settings. Usually, the ORM storage is used to integrate 294 credentials into an existing Django user system. 295 296 Returns: 297 A tuple containing three strings, or None. If 298 ``GOOGLE_OAUTH2_STORAGE_MODEL`` is configured, the tuple 299 will contain the fully qualifed path of the `django.db.model`, 300 the name of the ``django.contrib.auth.models.User`` field on the 301 model, and the name of the 302 :class:`oauth2client.contrib.django_util.models.CredentialsField` 303 field on the model. If Django ORM storage is not configured, 304 this function returns None. 305 """ 306 storage_model_settings = getattr(django.conf.settings, 307 'GOOGLE_OAUTH2_STORAGE_MODEL', None) 308 if storage_model_settings is not None: 309 return (storage_model_settings['model'], 310 storage_model_settings['user_property'], 311 storage_model_settings['credentials_property']) 312 else: 313 return None, None, None 314 315 316class OAuth2Settings(object): 317 """Initializes Django OAuth2 Helper Settings 318 319 This class loads the OAuth2 Settings from the Django settings, and then 320 provides those settings as attributes to the rest of the views and 321 decorators in the module. 322 323 Attributes: 324 scopes: A list of OAuth2 scopes that the decorators and views will use 325 as defaults. 326 request_prefix: The name of the attribute that the decorators use to 327 attach the UserOAuth2 object to the Django request object. 328 client_id: The OAuth2 Client ID. 329 client_secret: The OAuth2 Client Secret. 330 """ 331 332 def __init__(self, settings_instance): 333 self.scopes = getattr(settings_instance, 'GOOGLE_OAUTH2_SCOPES', 334 GOOGLE_OAUTH2_DEFAULT_SCOPES) 335 self.request_prefix = getattr(settings_instance, 336 'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE', 337 GOOGLE_OAUTH2_REQUEST_ATTRIBUTE) 338 self.client_id, self.client_secret = \ 339 _get_oauth2_client_id_and_secret(settings_instance) 340 341 if ('django.contrib.sessions.middleware.SessionMiddleware' 342 not in settings_instance.MIDDLEWARE_CLASSES): 343 raise exceptions.ImproperlyConfigured( 344 'The Google OAuth2 Helper requires session middleware to ' 345 'be installed. Edit your MIDDLEWARE_CLASSES setting' 346 ' to include \'django.contrib.sessions.middleware.' 347 'SessionMiddleware\'.') 348 (self.storage_model, self.storage_model_user_property, 349 self.storage_model_credentials_property) = _get_storage_model() 350 351 352oauth2_settings = OAuth2Settings(django.conf.settings) 353 354_CREDENTIALS_KEY = 'google_oauth2_credentials' 355 356 357def get_storage(request): 358 """ Gets a Credentials storage object provided by the Django OAuth2 Helper 359 object. 360 361 Args: 362 request: Reference to the current request object. 363 364 Returns: 365 An :class:`oauth2.client.Storage` object. 366 """ 367 storage_model = oauth2_settings.storage_model 368 user_property = oauth2_settings.storage_model_user_property 369 credentials_property = oauth2_settings.storage_model_credentials_property 370 371 if storage_model: 372 module_name, class_name = storage_model.rsplit('.', 1) 373 module = importlib.import_module(module_name) 374 storage_model_class = getattr(module, class_name) 375 return storage.DjangoORMStorage(storage_model_class, 376 user_property, 377 request.user, 378 credentials_property) 379 else: 380 # use session 381 return dictionary_storage.DictionaryStorage( 382 request.session, key=_CREDENTIALS_KEY) 383 384 385def _redirect_with_params(url_name, *args, **kwargs): 386 """Helper method to create a redirect response with URL params. 387 388 This builds a redirect string that converts kwargs into a 389 query string. 390 391 Args: 392 url_name: The name of the url to redirect to. 393 kwargs: the query string param and their values to build. 394 395 Returns: 396 A properly formatted redirect string. 397 """ 398 url = urlresolvers.reverse(url_name, args=args) 399 params = parse.urlencode(kwargs, True) 400 return "{0}?{1}".format(url, params) 401 402 403def _credentials_from_request(request): 404 """Gets the authorized credentials for this flow, if they exist.""" 405 # ORM storage requires a logged in user 406 if (oauth2_settings.storage_model is None or 407 request.user.is_authenticated()): 408 return get_storage(request).get() 409 else: 410 return None 411 412 413class UserOAuth2(object): 414 """Class to create oauth2 objects on Django request objects containing 415 credentials and helper methods. 416 """ 417 418 def __init__(self, request, scopes=None, return_url=None): 419 """Initialize the Oauth2 Object. 420 421 Args: 422 request: Django request object. 423 scopes: Scopes desired for this OAuth2 flow. 424 return_url: The url to return to after the OAuth flow is complete, 425 defaults to the request's current URL path. 426 """ 427 self.request = request 428 self.return_url = return_url or request.get_full_path() 429 if scopes: 430 self._scopes = set(oauth2_settings.scopes) | set(scopes) 431 else: 432 self._scopes = set(oauth2_settings.scopes) 433 434 def get_authorize_redirect(self): 435 """Creates a URl to start the OAuth2 authorization flow.""" 436 get_params = { 437 'return_url': self.return_url, 438 'scopes': self._get_scopes() 439 } 440 441 return _redirect_with_params('google_oauth:authorize', **get_params) 442 443 def has_credentials(self): 444 """Returns True if there are valid credentials for the current user 445 and required scopes.""" 446 credentials = _credentials_from_request(self.request) 447 return (credentials and not credentials.invalid and 448 credentials.has_scopes(self._get_scopes())) 449 450 def _get_scopes(self): 451 """Returns the scopes associated with this object, kept up to 452 date for incremental auth.""" 453 if _credentials_from_request(self.request): 454 return (self._scopes | 455 _credentials_from_request(self.request).scopes) 456 else: 457 return self._scopes 458 459 @property 460 def scopes(self): 461 """Returns the scopes associated with this OAuth2 object.""" 462 # make sure previously requested custom scopes are maintained 463 # in future authorizations 464 return self._get_scopes() 465 466 @property 467 def credentials(self): 468 """Gets the authorized credentials for this flow, if they exist.""" 469 return _credentials_from_request(self.request) 470 471 @property 472 def http(self): 473 """Helper method to create an HTTP client authorized with OAuth2 474 credentials.""" 475 if self.has_credentials(): 476 return self.credentials.authorize(httplib2.Http()) 477 return None 478