1# 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
17This module provides integration with `requests-oauthlib`_ for running the
18`OAuth 2.0 Authorization Flow`_ and acquiring user credentials. See
19`Using OAuth 2.0 to Access Google APIs`_ for an overview of OAuth 2.0
20authorization scenarios Google APIs support.
21
22Here's an example of using :class:`InstalledAppFlow`::
23
24 from google_auth_oauthlib.flow import InstalledAppFlow
25
26 # Create the flow using the client secrets file from the Google API
27 # Console.
28 flow = InstalledAppFlow.from_client_secrets_file(
29 'client_secrets.json',
30 scopes=['profile', 'email'])
31
32 flow.run_local_server()
33
34 # You can use flow.credentials, or you can just get a requests session
35 # using flow.authorized_session.
36 session = flow.authorized_session()
37
38 profile_info = session.get(
39 'https://www.googleapis.com/userinfo/v2/me').json()
40
41 print(profile_info)
42 # {'name': '...', 'email': '...', ...}
43
44.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/latest/
45.. _OAuth 2.0 Authorization Flow:
46 https://tools.ietf.org/html/rfc6749#section-1.2
47.. _Using OAuth 2.0 to Access Google APIs:
48 https://developers.google.com/identity/protocols/oauth2
49
50"""
51from base64 import urlsafe_b64encode
52import hashlib
53import json
54import logging
55
56try:
57 from secrets import SystemRandom
58except ImportError: # pragma: NO COVER
59 from random import SystemRandom
60from string import ascii_letters, digits
61import webbrowser
62import wsgiref.simple_server
63import wsgiref.util
64
65import google.auth.transport.requests
66import google.oauth2.credentials
67
68import google_auth_oauthlib.helpers
69
70
71_LOGGER = logging.getLogger(__name__)
72
73
74class Flow(object):
75 """OAuth 2.0 Authorization Flow
76
77 This class uses a :class:`requests_oauthlib.OAuth2Session` instance at
78 :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class
79 just provides convenience methods and sane defaults for doing Google's
80 particular flavors of OAuth 2.0.
81
82 Typically you'll construct an instance of this flow using
83 :meth:`from_client_secrets_file` and a `client secrets file`_ obtained
84 from the `Google API Console`_.
85
86 .. _client secrets file:
87 https://developers.google.com/identity/protocols/oauth2/web-server
88 #creatingcred
89 .. _Google API Console:
90 https://console.developers.google.com/apis/credentials
91 """
92
93 def __init__(
94 self,
95 oauth2session,
96 client_type,
97 client_config,
98 redirect_uri=None,
99 code_verifier=None,
100 autogenerate_code_verifier=True,
101 ):
102 """
103 Args:
104 oauth2session (requests_oauthlib.OAuth2Session):
105 The OAuth 2.0 session from ``requests-oauthlib``.
106 client_type (str): The client type, either ``web`` or
107 ``installed``.
108 client_config (Mapping[str, Any]): The client
109 configuration in the Google `client secrets`_ format.
110 redirect_uri (str): The OAuth 2.0 redirect URI if known at flow
111 creation time. Otherwise, it will need to be set using
112 :attr:`redirect_uri`.
113 code_verifier (str): random string of 43-128 chars used to verify
114 the key exchange.using PKCE.
115 autogenerate_code_verifier (bool): If true, auto-generate a
116 code_verifier.
117 .. _client secrets:
118 https://github.com/googleapis/google-api-python-client/blob
119 /main/docs/client-secrets.md
120 """
121 self.client_type = client_type
122 """str: The client type, either ``'web'`` or ``'installed'``"""
123 self.client_config = client_config[client_type]
124 """Mapping[str, Any]: The OAuth 2.0 client configuration."""
125 self.oauth2session = oauth2session
126 """requests_oauthlib.OAuth2Session: The OAuth 2.0 session."""
127 self.redirect_uri = redirect_uri
128 self.code_verifier = code_verifier
129 self.autogenerate_code_verifier = autogenerate_code_verifier
130
131 @classmethod
132 def from_client_config(cls, client_config, scopes, **kwargs):
133 """Creates a :class:`requests_oauthlib.OAuth2Session` from client
134 configuration loaded from a Google-format client secrets file.
135
136 Args:
137 client_config (Mapping[str, Any]): The client
138 configuration in the Google `client secrets`_ format.
139 scopes (Sequence[str]): The list of scopes to request during the
140 flow.
141 kwargs: Any additional parameters passed to
142 :class:`requests_oauthlib.OAuth2Session`
143
144 Returns:
145 Flow: The constructed Flow instance.
146
147 Raises:
148 ValueError: If the client configuration is not in the correct
149 format.
150
151 .. _client secrets:
152 https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md
153 """
154 if "web" in client_config:
155 client_type = "web"
156 elif "installed" in client_config:
157 client_type = "installed"
158 else:
159 raise ValueError("Client secrets must be for a web or installed app.")
160
161 # these args cannot be passed to requests_oauthlib.OAuth2Session
162 code_verifier = kwargs.pop("code_verifier", None)
163 autogenerate_code_verifier = kwargs.pop("autogenerate_code_verifier", None)
164
165 (
166 session,
167 client_config,
168 ) = google_auth_oauthlib.helpers.session_from_client_config(
169 client_config, scopes, **kwargs
170 )
171
172 redirect_uri = kwargs.get("redirect_uri", None)
173
174 return cls(
175 session,
176 client_type,
177 client_config,
178 redirect_uri,
179 code_verifier,
180 autogenerate_code_verifier,
181 )
182
183 @classmethod
184 def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs):
185 """Creates a :class:`Flow` instance from a Google client secrets file.
186
187 Args:
188 client_secrets_file (str): The path to the client secrets .json
189 file.
190 scopes (Sequence[str]): The list of scopes to request during the
191 flow.
192 kwargs: Any additional parameters passed to
193 :class:`requests_oauthlib.OAuth2Session`
194
195 Returns:
196 Flow: The constructed Flow instance.
197 """
198 with open(client_secrets_file, "r") as json_file:
199 client_config = json.load(json_file)
200
201 return cls.from_client_config(client_config, scopes=scopes, **kwargs)
202
203 @property
204 def redirect_uri(self):
205 """The OAuth 2.0 redirect URI. Pass-through to
206 ``self.oauth2session.redirect_uri``."""
207 return self.oauth2session.redirect_uri
208
209 @redirect_uri.setter
210 def redirect_uri(self, value):
211 """The OAuth 2.0 redirect URI. Pass-through to
212 ``self.oauth2session.redirect_uri``."""
213 self.oauth2session.redirect_uri = value
214
215 def authorization_url(self, **kwargs):
216 """Generates an authorization URL.
217
218 This is the first step in the OAuth 2.0 Authorization Flow. The user's
219 browser should be redirected to the returned URL.
220
221 This method calls
222 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
223 and specifies the client configuration's authorization URI (usually
224 Google's authorization server) and specifies that "offline" access is
225 desired. This is required in order to obtain a refresh token.
226
227 Args:
228 kwargs: Additional arguments passed through to
229 :meth:`requests_oauthlib.OAuth2Session.authorization_url`
230
231 Returns:
232 Tuple[str, str]: The generated authorization URL and state. The
233 user must visit the URL to complete the flow. The state is used
234 when completing the flow to verify that the request originated
235 from your application. If your application is using a different
236 :class:`Flow` instance to obtain the token, you will need to
237 specify the ``state`` when constructing the :class:`Flow`.
238 """
239 kwargs.setdefault("access_type", "offline")
240 if self.autogenerate_code_verifier:
241 chars = ascii_letters + digits + "-._~"
242 rnd = SystemRandom()
243 random_verifier = [rnd.choice(chars) for _ in range(0, 128)]
244 self.code_verifier = "".join(random_verifier)
245
246 if self.code_verifier:
247 code_hash = hashlib.sha256()
248 code_hash.update(str.encode(self.code_verifier))
249 unencoded_challenge = code_hash.digest()
250 b64_challenge = urlsafe_b64encode(unencoded_challenge)
251 code_challenge = b64_challenge.decode().split("=")[0]
252 kwargs.setdefault("code_challenge", code_challenge)
253 kwargs.setdefault("code_challenge_method", "S256")
254 url, state = self.oauth2session.authorization_url(
255 self.client_config["auth_uri"], **kwargs
256 )
257
258 return url, state
259
260 def fetch_token(self, **kwargs):
261 """Completes the Authorization Flow and obtains an access token.
262
263 This is the final step in the OAuth 2.0 Authorization Flow. This is
264 called after the user consents.
265
266 This method calls
267 :meth:`requests_oauthlib.OAuth2Session.fetch_token`
268 and specifies the client configuration's token URI (usually Google's
269 token server).
270
271 Args:
272 kwargs: Arguments passed through to
273 :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least
274 one of ``code`` or ``authorization_response`` must be
275 specified.
276
277 Returns:
278 Mapping[str, str]: The obtained tokens. Typically, you will not use
279 return value of this function and instead use
280 :meth:`credentials` to obtain a
281 :class:`~google.auth.credentials.Credentials` instance.
282 """
283 kwargs.setdefault("client_secret", self.client_config["client_secret"])
284 kwargs.setdefault("code_verifier", self.code_verifier)
285 return self.oauth2session.fetch_token(self.client_config["token_uri"], **kwargs)
286
287 @property
288 def credentials(self):
289 """Returns credentials from the OAuth 2.0 session.
290
291 :meth:`fetch_token` must be called before accessing this. This method
292 constructs a :class:`google.oauth2.credentials.Credentials` class using
293 the session's token and the client config.
294
295 Returns:
296 google.oauth2.credentials.Credentials: The constructed credentials.
297
298 Raises:
299 ValueError: If there is no access token in the session.
300 """
301 return google_auth_oauthlib.helpers.credentials_from_session(
302 self.oauth2session, self.client_config
303 )
304
305 def authorized_session(self):
306 """Returns a :class:`requests.Session` authorized with credentials.
307
308 :meth:`fetch_token` must be called before this method. This method
309 constructs a :class:`google.auth.transport.requests.AuthorizedSession`
310 class using this flow's :attr:`credentials`.
311
312 Returns:
313 google.auth.transport.requests.AuthorizedSession: The constructed
314 session.
315 """
316 return google.auth.transport.requests.AuthorizedSession(self.credentials)
317
318
319class InstalledAppFlow(Flow):
320 """Authorization flow helper for installed applications.
321
322 This :class:`Flow` subclass makes it easier to perform the
323 `Installed Application Authorization Flow`_. This flow is useful for
324 local development or applications that are installed on a desktop operating
325 system.
326
327 This flow uses a local server strategy provided by :meth:`run_local_server`.
328
329 Example::
330
331 from google_auth_oauthlib.flow import InstalledAppFlow
332
333 flow = InstalledAppFlow.from_client_secrets_file(
334 'client_secrets.json',
335 scopes=['profile', 'email'])
336
337 flow.run_local_server()
338
339 session = flow.authorized_session()
340
341 profile_info = session.get(
342 'https://www.googleapis.com/userinfo/v2/me').json()
343
344 print(profile_info)
345 # {'name': '...', 'email': '...', ...}
346
347
348 Note that this isn't the only way to accomplish the installed
349 application flow, just one of the most common. You can use the
350 :class:`Flow` class to perform the same flow with different methods of
351 presenting the authorization URL to the user or obtaining the authorization
352 response, such as using an embedded web view.
353
354 .. _Installed Application Authorization Flow:
355 https://github.com/googleapis/google-api-python-client/blob/main/docs/oauth-installed.md
356 """
357
358 _DEFAULT_AUTH_PROMPT_MESSAGE = (
359 "Please visit this URL to authorize this application: {url}"
360 )
361 """str: The message to display when prompting the user for
362 authorization."""
363 _DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: "
364 """str: The message to display when prompting the user for the
365 authorization code. Used only by the console strategy."""
366
367 _DEFAULT_WEB_SUCCESS_MESSAGE = (
368 "The authentication flow has completed. You may close this window."
369 )
370
371 def run_local_server(
372 self,
373 host="localhost",
374 bind_addr=None,
375 port=8080,
376 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
377 success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
378 open_browser=True,
379 redirect_uri_trailing_slash=True,
380 timeout_seconds=None,
381 token_audience=None,
382 browser=None,
383 **kwargs
384 ):
385 """Run the flow using the server strategy.
386
387 The server strategy instructs the user to open the authorization URL in
388 their browser and will attempt to automatically open the URL for them.
389 It will start a local web server to listen for the authorization
390 response. Once authorization is complete the authorization server will
391 redirect the user's browser to the local web server. The web server
392 will get the authorization code from the response and shutdown. The
393 code is then exchanged for a token.
394
395 Args:
396 host (str): The hostname for the local redirect server. This will
397 be served over http, not https.
398 bind_addr (str): Optionally provide an ip address for the redirect
399 server to listen on when it is not the same as host
400 (e.g. in a container). Default value is None,
401 which means that the redirect server will listen
402 on the ip address specified in the host parameter.
403 port (int): The port for the local redirect server.
404 authorization_prompt_message (str | None): The message to display to tell
405 the user to navigate to the authorization URL. If None or empty,
406 don't display anything.
407 success_message (str): The message to display in the web browser
408 the authorization flow is complete.
409 open_browser (bool): Whether or not to open the authorization URL
410 in the user's browser.
411 redirect_uri_trailing_slash (bool): whether or not to add trailing
412 slash when constructing the redirect_uri. Default value is True.
413 timeout_seconds (int): It will raise an error after the timeout timing
414 if there are no credentials response. The value is in seconds.
415 When set to None there is no timeout.
416 Default value is None.
417 token_audience (str): Passed along with the request for an access
418 token. Determines the endpoints with which the token can be
419 used. Optional.
420 browser (str): specify which browser to open for authentication. If not
421 specified this defaults to default browser.
422 kwargs: Additional keyword arguments passed through to
423 :meth:`authorization_url`.
424
425 Returns:
426 google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
427 for the user.
428 """
429 wsgi_app = _RedirectWSGIApp(success_message)
430 # Fail fast if the address is occupied
431 wsgiref.simple_server.WSGIServer.allow_reuse_address = False
432 local_server = wsgiref.simple_server.make_server(
433 bind_addr or host, port, wsgi_app, handler_class=_WSGIRequestHandler
434 )
435
436 try:
437 redirect_uri_format = (
438 "http://{}:{}/" if redirect_uri_trailing_slash else "http://{}:{}"
439 )
440 self.redirect_uri = redirect_uri_format.format(
441 host, local_server.server_port
442 )
443 auth_url, _ = self.authorization_url(**kwargs)
444
445 if open_browser:
446 # if browser is None it defaults to default browser
447 webbrowser.get(browser).open(auth_url, new=1, autoraise=True)
448
449 if authorization_prompt_message:
450 print(authorization_prompt_message.format(url=auth_url))
451
452 local_server.timeout = timeout_seconds
453 local_server.handle_request()
454
455 # Note: using https here because oauthlib is very picky that
456 # OAuth 2.0 should only occur over https.
457 authorization_response = wsgi_app.last_request_uri.replace("http", "https")
458 self.fetch_token(
459 authorization_response=authorization_response, audience=token_audience
460 )
461 finally:
462 local_server.server_close()
463
464 return self.credentials
465
466
467class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
468 """Custom WSGIRequestHandler.
469
470 Uses a named logger instead of printing to stderr.
471 """
472
473 def log_message(self, format, *args):
474 # pylint: disable=redefined-builtin
475 # (format is the argument name defined in the superclass.)
476 _LOGGER.info(format, *args)
477
478
479class _RedirectWSGIApp(object):
480 """WSGI app to handle the authorization redirect.
481
482 Stores the request URI and displays the given success message.
483 """
484
485 def __init__(self, success_message):
486 """
487 Args:
488 success_message (str): The message to display in the web browser
489 the authorization flow is complete.
490 """
491 self.last_request_uri = None
492 self._success_message = success_message
493
494 def __call__(self, environ, start_response):
495 """WSGI Callable.
496
497 Args:
498 environ (Mapping[str, Any]): The WSGI environment.
499 start_response (Callable[str, list]): The WSGI start_response
500 callable.
501
502 Returns:
503 Iterable[bytes]: The response body.
504 """
505 start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
506 self.last_request_uri = wsgiref.util.request_uri(environ)
507 return [self._success_message.encode("utf-8")]