blob: a4dcfca77ff692e72ac0ca869e847ee820de395e [file] [log] [blame]
Jon Wayne Parrott4382bc12017-01-10 13:35:51 -08001# Copyright 2016 Google Inc.
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"""OAuth 2.0 Authorization Flow
16
Jon Wayne Parrott474c8df2017-02-08 13:14:02 -080017.. warning::
18 This module is experimental and is subject to change signficantly
19 within major version releases.
20
Jon Wayne Parrott4382bc12017-01-10 13:35:51 -080021This module provides integration with `requests-oauthlib`_ for running the
22`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.
23
24Here's an example of using the flow with the installed application
25authorization flow::
26
27 import google.oauth2.flow
28
29 # Create the flow using the client secrets file from the Google API
30 # Console.
31 flow = google.oauth2.flow.Flow.from_client_secrets_file(
32 'path/to/client_secrets.json',
33 scopes=['profile', 'email'],
34 redirect_uri='urn:ietf:wg:oauth:2.0:oob')
35
36 # Tell the user to go to the authorization URL.
37 auth_url, _ = flow.authorization_url(prompt='consent')
38
39 print('Please go to this URL: {}'.format(auth_url))
40
41 # The user will get an authorization code. This code is used to get the
42 # access token.
43 code = input('Enter the authorization code: ')
44 flow.fetch_token(code=code)
45
46 # You can use flow.credentials, or you can just get a requests session
47 # using flow.authorized_session.
48 session = flow.authorized_session()
49 print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
50
51.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
52.. _OAuth 2.0 Authorization Flow:
53 https://tools.ietf.org/html/rfc6749#section-1.2
54"""
55
56import json
57
58import requests_oauthlib
59
60import google.auth.transport.requests
61import google.oauth2.credentials
62
63_REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
64
65
66class Flow(object):
67 """OAuth 2.0 Authorization Flow
68
69 This class uses a :class:`requests_oauthlib.OAuth2Session` instance at
70 :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class
71 just provides convenience methods and sane defaults for doing Google's
72 particular flavors of OAuth 2.0.
73
74 Typically you'll construct an instance of this flow using
75 :meth:`from_client_secrets_file` and a `client secrets file`_ obtained
76 from the `Google API Console`_.
77
78 .. _client secrets file:
79 https://developers.google.com/identity/protocols/OAuth2WebServer
80 #creatingcred
81 .. _Google API Console:
82 https://console.developers.google.com/apis/credentials
83 """
84
85 def __init__(self, client_config, scopes, **kwargs):
86 """
87 Args:
88 client_config (Mapping[str, Any]): The client
89 configuration in the Google `client secrets`_ format.
90 scopes (Sequence[str]): The list of scopes to request during the
91 flow.
92 kwargs: Any additional parameters passed to
93 :class:`requests_oauthlib.OAuth2Session`
94
95 Raises:
96 ValueError: If the client configuration is not in the correct
97 format.
98
99 .. _client secrets:
100 https://developers.google.com/api-client-library/python/guide
101 /aaa_client_secrets
102 """
103 self.client_config = None
104 """Mapping[str, Any]: The OAuth 2.0 client configuration."""
105 self.client_type = None
106 """str: The client type, either ``'web'`` or ``'installed'``"""
107
108 if 'web' in client_config:
109 self.client_config = client_config['web']
110 self.client_type = 'web'
111 elif 'installed' in client_config:
112 self.client_config = client_config['installed']
113 self.client_type = 'installed'
114 else:
115 raise ValueError(
116 'Client secrets must be for a web or installed app.')
117
118 if not _REQUIRED_CONFIG_KEYS.issubset(self.client_config.keys()):
119 raise ValueError('Client secrets is not in the correct format.')
120
121 self.oauth2session = requests_oauthlib.OAuth2Session(
122 client_id=self.client_config['client_id'],
123 scope=scopes,
124 **kwargs)
125 """requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
126
127 @classmethod
128 def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
129 """Creates a :class:`Flow` instance from a Google client secrets file.
130
131 Args:
132 client_secrets_file (str): The path to the client secrets .json
133 file.
134 scopes (Sequence[str]): The list of scopes to request during the
135 flow.
136 kwargs: Any additional parameters passed to
137 :class:`requests_oauthlib.OAuth2Session`
138
139 Returns:
140 Flow: The constructed Flow instance.
141 """
142 with open(client_secrets_file, 'r') as json_file:
143 client_config = json.load(json_file)
144
145 return cls(client_config, scopes=scopes, **kwargs)
146
147 @property
148 def redirect_uri(self):
149 """The OAuth 2.0 redirect URI. Pass-through to
150 ``self.oauth2session.redirect_uri``."""
151 return self.oauth2session.redirect_uri
152
153 @redirect_uri.setter
154 def redirect_uri(self, value):
155 self.oauth2session.redirect_uri = value
156
157 def authorization_url(self, **kwargs):
158 """Generates an authorization URL.
159
160 This is the first step in the OAuth 2.0 Authorization Flow. The user's
161 browser should be redirected to the returned URL.
162
163 This method calls
164 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
165 and specifies the client configuration's authorization URI (usually
166 Google's authorization server) and specifies that "offline" access is
167 desired. This is required in order to obtain a refresh token.
168
169 Args:
170 kwargs: Additional arguments passed through to
171 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
172
173 Returns:
174 Tuple[str, str]: The generated authorization URL and state. The
175 user must visit the URL to complete the flow. The state is used
176 when completing the flow to verify that the request originated
177 from your application. If your application is using a different
178 :class:`Flow` instance to obtain the token, you will need to
179 specify the ``state`` when constructing the :class:`Flow`.
180 """
181 url, state = self.oauth2session.authorization_url(
182 self.client_config['auth_uri'],
183 access_type='offline', **kwargs)
184
185 return url, state
186
187 def fetch_token(self, **kwargs):
188 """Completes the Authorization Flow and obtains an access token.
189
190 This is the final step in the OAuth 2.0 Authorization Flow. This is
191 called after the user consents.
192
193 This method calls
194 :meth:`requests_oauthlib.OAuth2Session.fetch_token`
195 and specifies the client configuration's token URI (usually Google's
196 token server).
197
198 Args:
199 kwargs: Arguments passed through to
200 :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least
201 one of ``code`` or ``authorization_response`` must be
202 specified.
203
204 Returns:
205 Mapping[str, str]: The obtained tokens. Typically, you will not use
206 return value of this function and instead and use
207 :meth:`credentials` to obtain a
208 :class:`~google.auth.credentials.Credentials` instance.
209 """
210 return self.oauth2session.fetch_token(
211 self.client_config['token_uri'],
212 client_secret=self.client_config['client_secret'],
213 **kwargs)
214
215 @property
216 def credentials(self):
217 """Returns credentials from the OAuth 2.0 session.
218
219 :meth:`fetch_token` must be called before accessing this. This method
220 constructs a :class:`google.oauth2.credentials.Credentials` class using
221 the session's token and the client config.
222
223 Returns:
224 google.oauth2.credentials.Credentials: The constructed credentials.
225
226 Raises:
227 ValueError: If there is no access token in the session.
228 """
229 if not self.oauth2session.token:
230 raise ValueError(
231 'There is no access token for this session, did you call '
232 'fetch_token?')
233
234 return google.oauth2.credentials.Credentials(
235 self.oauth2session.token['access_token'],
236 refresh_token=self.oauth2session.token['refresh_token'],
237 token_uri=self.client_config['token_uri'],
238 client_id=self.client_config['client_id'],
239 client_secret=self.client_config['client_secret'],
240 scopes=self.oauth2session.scope)
241
242 def authorized_session(self):
243 """Returns a :class:`requests.Session` authorized with credentials.
244
245 :meth:`fetch_token` must be called before this method. This method
246 constructs a :class:`google.auth.transport.requests.AuthorizedSession`
247 class using this flow's :attr:`credentials`.
248
249 Returns:
250 google.auth.transport.requests.AuthorizedSession: The constructed
251 session.
252 """
253 return google.auth.transport.requests.AuthorizedSession(
254 self.credentials)