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