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"""This module contains the views used by the OAuth2 flows. 16 17Their are two views used by the OAuth2 flow, the authorize and the callback 18view. The authorize view kicks off the three-legged OAuth flow, and the 19callback view validates the flow and if successful stores the credentials 20in the configured storage.""" 21 22import hashlib 23import json 24import os 25import pickle 26 27from django import http 28from django import shortcuts 29from django.conf import settings 30from django.core import urlresolvers 31from django.shortcuts import redirect 32from six.moves.urllib import parse 33 34from oauth2client import client 35from oauth2client.contrib import django_util 36from oauth2client.contrib.django_util import get_storage 37from oauth2client.contrib.django_util import signals 38 39_CSRF_KEY = 'google_oauth2_csrf_token' 40_FLOW_KEY = 'google_oauth2_flow_{0}' 41 42 43def _make_flow(request, scopes, return_url=None): 44 """Creates a Web Server Flow 45 46 Args: 47 request: A Django request object. 48 scopes: the request oauth2 scopes. 49 return_url: The URL to return to after the flow is complete. Defaults 50 to the path of the current request. 51 52 Returns: 53 An OAuth2 flow object that has been stored in the session. 54 """ 55 # Generate a CSRF token to prevent malicious requests. 56 csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest() 57 58 request.session[_CSRF_KEY] = csrf_token 59 60 state = json.dumps({ 61 'csrf_token': csrf_token, 62 'return_url': return_url, 63 }) 64 65 flow = client.OAuth2WebServerFlow( 66 client_id=django_util.oauth2_settings.client_id, 67 client_secret=django_util.oauth2_settings.client_secret, 68 scope=scopes, 69 state=state, 70 redirect_uri=request.build_absolute_uri( 71 urlresolvers.reverse("google_oauth:callback"))) 72 73 flow_key = _FLOW_KEY.format(csrf_token) 74 request.session[flow_key] = pickle.dumps(flow) 75 return flow 76 77 78def _get_flow_for_token(csrf_token, request): 79 """ Looks up the flow in session to recover information about requested 80 scopes. 81 82 Args: 83 csrf_token: The token passed in the callback request that should 84 match the one previously generated and stored in the request on the 85 initial authorization view. 86 87 Returns: 88 The OAuth2 Flow object associated with this flow based on the 89 CSRF token. 90 """ 91 flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None) 92 return None if flow_pickle is None else pickle.loads(flow_pickle) 93 94 95def oauth2_callback(request): 96 """ View that handles the user's return from OAuth2 provider. 97 98 This view verifies the CSRF state and OAuth authorization code, and on 99 success stores the credentials obtained in the storage provider, 100 and redirects to the return_url specified in the authorize view and 101 stored in the session. 102 103 Args: 104 request: Django request. 105 106 Returns: 107 A redirect response back to the return_url. 108 """ 109 if 'error' in request.GET: 110 reason = request.GET.get( 111 'error_description', request.GET.get('error', '')) 112 return http.HttpResponseBadRequest( 113 'Authorization failed {0}'.format(reason)) 114 115 try: 116 encoded_state = request.GET['state'] 117 code = request.GET['code'] 118 except KeyError: 119 return http.HttpResponseBadRequest( 120 'Request missing state or authorization code') 121 122 try: 123 server_csrf = request.session[_CSRF_KEY] 124 except KeyError: 125 return http.HttpResponseBadRequest( 126 'No existing session for this flow.') 127 128 try: 129 state = json.loads(encoded_state) 130 client_csrf = state['csrf_token'] 131 return_url = state['return_url'] 132 except (ValueError, KeyError): 133 return http.HttpResponseBadRequest('Invalid state parameter.') 134 135 if client_csrf != server_csrf: 136 return http.HttpResponseBadRequest('Invalid CSRF token.') 137 138 flow = _get_flow_for_token(client_csrf, request) 139 140 if not flow: 141 return http.HttpResponseBadRequest('Missing Oauth2 flow.') 142 143 try: 144 credentials = flow.step2_exchange(code) 145 except client.FlowExchangeError as exchange_error: 146 return http.HttpResponseBadRequest( 147 'An error has occurred: {0}'.format(exchange_error)) 148 149 get_storage(request).put(credentials) 150 151 signals.oauth2_authorized.send(sender=signals.oauth2_authorized, 152 request=request, credentials=credentials) 153 154 return shortcuts.redirect(return_url) 155 156 157def oauth2_authorize(request): 158 """ View to start the OAuth2 Authorization flow. 159 160 This view starts the OAuth2 authorization flow. If scopes is passed in 161 as a GET URL parameter, it will authorize those scopes, otherwise the 162 default scopes specified in settings. The return_url can also be 163 specified as a GET parameter, otherwise the referer header will be 164 checked, and if that isn't found it will return to the root path. 165 166 Args: 167 request: The Django request object. 168 169 Returns: 170 A redirect to Google OAuth2 Authorization. 171 """ 172 return_url = request.GET.get('return_url', None) 173 174 # Model storage (but not session storage) requires a logged in user 175 if django_util.oauth2_settings.storage_model: 176 if not request.user.is_authenticated(): 177 return redirect('{0}?next={1}'.format( 178 settings.LOGIN_URL, parse.quote(request.get_full_path()))) 179 # This checks for the case where we ended up here because of a logged 180 # out user but we had credentials for it in the first place 181 elif get_storage(request).get() is not None: 182 return redirect(return_url) 183 184 scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes) 185 186 if not return_url: 187 return_url = request.META.get('HTTP_REFERER', '/') 188 flow = _make_flow(request=request, scopes=scopes, return_url=return_url) 189 auth_url = flow.step1_get_authorize_url() 190 return shortcuts.redirect(auth_url) 191