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 
17 Their are two views used by the OAuth2 flow, the authorize and the callback
18 view. The authorize view kicks off the three-legged OAuth flow, and the
19 callback view validates the flow and if successful stores the credentials
20 in the configured storage."""
21 
22 import hashlib
23 import json
24 import os
25 import pickle
26 
27 from django import http
28 from django import shortcuts
29 from django.conf import settings
30 from django.core import urlresolvers
31 from django.shortcuts import redirect
32 from six.moves.urllib import parse
33 
34 from oauth2client import client
35 from oauth2client.contrib import django_util
36 from oauth2client.contrib.django_util import get_storage
37 from oauth2client.contrib.django_util import signals
38 
39 _CSRF_KEY = 'google_oauth2_csrf_token'
40 _FLOW_KEY = 'google_oauth2_flow_{0}'
41 
42 
43 def _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 
78 def _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 
95 def 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 
157 def 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