New Upstream Release - google-auth-oauthlib

Ready changes

Summary

Merged new upstream version: 1.0.0 (was: 0.4.2).

Diff

diff --git a/LICENSE b/LICENSE
index a8ee855..d645695 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
-                            Apache License
+
+                                 Apache License
                            Version 2.0, January 2004
-                        https://www.apache.org/licenses/
+                        http://www.apache.org/licenses/
 
    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
@@ -192,7 +193,7 @@
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
 
-       https://www.apache.org/licenses/LICENSE-2.0
+       http://www.apache.org/licenses/LICENSE-2.0
 
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/MANIFEST.in b/MANIFEST.in
index e9e29d1..e783f4c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -16,10 +16,10 @@
 
 # Generated by synthtool. DO NOT EDIT!
 include README.rst LICENSE
-recursive-include google *.json *.proto
+recursive-include google *.json *.proto py.typed
 recursive-include tests *
 global-exclude *.py[co]
 global-exclude __pycache__
 
 # Exclude scripts for samples readmegen
-prune scripts/readme-gen
\ No newline at end of file
+prune scripts/readme-gen
diff --git a/PKG-INFO b/PKG-INFO
index 0da3a6e..056c0ef 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,59 +1,20 @@
 Metadata-Version: 2.1
 Name: google-auth-oauthlib
-Version: 0.4.2
+Version: 1.0.0
 Summary: Google Authentication Library
 Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib
 Author: Google Cloud Platform
 Author-email: jonwayne+google-auth@google.com
 License: Apache 2.0
-Description: oauthlib integration for Google Auth
-        ====================================
-        
-        |pypi|
-        
-        This library provides `oauthlib`_ integration with `google-auth`_.
-        
-        .. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=master
-           :target: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib
-        .. |docs| image:: https://readthedocs.org/projects/google-auth-oauthlib/badge/?version=latest
-           :target: https://google-auth-oauthlib.readthedocs.io/en/latest/
-        .. |pypi| image:: https://img.shields.io/pypi/v/google-auth-oauthlib.svg
-           :target: https://pypi.python.org/pypi/google-auth-oauthlib
-        
-        .. _oauthlib: https://github.com/idan/oauthlib
-        .. _google-auth: https://github.com/googleapis/google-auth-library-python
-        
-        Installing
-        ----------
-        
-        You can install using `pip`_::
-        
-            $ pip install google-auth-oauthlib
-        
-        .. _pip: https://pip.pypa.io/en/stable/
-        
-        Documentation
-        -------------
-        
-        The latest documentation is available at `google-auth-oauthlib.readthedocs.io`_.
-        
-        .. _google-auth-oauthlib.readthedocs.io: http://google-auth-oauthlib.readthedocs.io/
-        
-        License
-        -------
-        
-        Apache 2.0 - See `the LICENSE`_ for more information.
-        
-        .. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/master/LICENSE
-        
 Keywords: google auth oauth client oauthlib
-Platform: UNKNOWN
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
-Classifier: Development Status :: 3 - Alpha
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: POSIX
@@ -63,3 +24,55 @@ Classifier: Operating System :: OS Independent
 Classifier: Topic :: Internet :: WWW/HTTP
 Requires-Python: >=3.6
 Provides-Extra: tool
+License-File: LICENSE
+
+oauthlib integration for Google Auth
+====================================
+
+|pypi|
+
+This library provides `oauthlib`_ integration with `google-auth`_.
+
+.. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=main
+   :target: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
+.. |pypi| image:: https://img.shields.io/pypi/v/google-auth-oauthlib.svg
+   :target: https://pypi.python.org/pypi/google-auth-oauthlib
+
+.. _oauthlib: https://github.com/idan/oauthlib
+.. _google-auth: https://github.com/googleapis/google-auth-library-python
+
+Installing
+----------
+
+You can install using `pip`_::
+
+    $ pip install google-auth-oauthlib
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+Documentation
+-------------
+
+The latest documentation is available at `google-auth-oauthlib.googleapis.dev`_.
+
+.. _google-auth-oauthlib.googleapis.dev: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
+
+Supported Python Versions
+-------------------------
+Python >= 3.6
+
+
+Unsupported Python Versions
+---------------------------
+
+Python == 2.7, Python == 3.5.
+
+The last version of this library compatible with Python 2.7 and 3.5 is
+`google-auth-oauthlib==0.4.1`.
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/main/LICENSE
diff --git a/README.rst b/README.rst
index e136b37..efe263f 100644
--- a/README.rst
+++ b/README.rst
@@ -5,10 +5,8 @@ oauthlib integration for Google Auth
 
 This library provides `oauthlib`_ integration with `google-auth`_.
 
-.. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=master
-   :target: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib
-.. |docs| image:: https://readthedocs.org/projects/google-auth-oauthlib/badge/?version=latest
-   :target: https://google-auth-oauthlib.readthedocs.io/en/latest/
+.. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=main
+   :target: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
 .. |pypi| image:: https://img.shields.io/pypi/v/google-auth-oauthlib.svg
    :target: https://pypi.python.org/pypi/google-auth-oauthlib
 
@@ -27,13 +25,26 @@ You can install using `pip`_::
 Documentation
 -------------
 
-The latest documentation is available at `google-auth-oauthlib.readthedocs.io`_.
+The latest documentation is available at `google-auth-oauthlib.googleapis.dev`_.
 
-.. _google-auth-oauthlib.readthedocs.io: http://google-auth-oauthlib.readthedocs.io/
+.. _google-auth-oauthlib.googleapis.dev: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
+
+Supported Python Versions
+-------------------------
+Python >= 3.6
+
+
+Unsupported Python Versions
+---------------------------
+
+Python == 2.7, Python == 3.5.
+
+The last version of this library compatible with Python 2.7 and 3.5 is
+`google-auth-oauthlib==0.4.1`.
 
 License
 -------
 
 Apache 2.0 - See `the LICENSE`_ for more information.
 
-.. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/master/LICENSE
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/main/LICENSE
diff --git a/debian/changelog b/debian/changelog
index d2abc51..7f10348 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,12 +1,13 @@
-google-auth-oauthlib (0.4.2-2) UNRELEASED; urgency=medium
+google-auth-oauthlib (1.0.0-1) UNRELEASED; urgency=medium
 
   * Set upstream metadata fields: Repository, Repository-Browse.
   * Update standards version to 4.5.1, no changes needed.
   * Avoid pypi.org in Homepage field.
   * Set upstream metadata fields: Repository-Browse.
   * Update standards version to 4.6.2, no changes needed.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Wed, 03 Feb 2021 00:01:33 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 21 Apr 2023 09:46:47 -0000
 
 google-auth-oauthlib (0.4.2-1) unstable; urgency=medium
 
diff --git a/google_auth_oauthlib.egg-info/PKG-INFO b/google_auth_oauthlib.egg-info/PKG-INFO
index 0da3a6e..056c0ef 100644
--- a/google_auth_oauthlib.egg-info/PKG-INFO
+++ b/google_auth_oauthlib.egg-info/PKG-INFO
@@ -1,59 +1,20 @@
 Metadata-Version: 2.1
 Name: google-auth-oauthlib
-Version: 0.4.2
+Version: 1.0.0
 Summary: Google Authentication Library
 Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib
 Author: Google Cloud Platform
 Author-email: jonwayne+google-auth@google.com
 License: Apache 2.0
-Description: oauthlib integration for Google Auth
-        ====================================
-        
-        |pypi|
-        
-        This library provides `oauthlib`_ integration with `google-auth`_.
-        
-        .. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=master
-           :target: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib
-        .. |docs| image:: https://readthedocs.org/projects/google-auth-oauthlib/badge/?version=latest
-           :target: https://google-auth-oauthlib.readthedocs.io/en/latest/
-        .. |pypi| image:: https://img.shields.io/pypi/v/google-auth-oauthlib.svg
-           :target: https://pypi.python.org/pypi/google-auth-oauthlib
-        
-        .. _oauthlib: https://github.com/idan/oauthlib
-        .. _google-auth: https://github.com/googleapis/google-auth-library-python
-        
-        Installing
-        ----------
-        
-        You can install using `pip`_::
-        
-            $ pip install google-auth-oauthlib
-        
-        .. _pip: https://pip.pypa.io/en/stable/
-        
-        Documentation
-        -------------
-        
-        The latest documentation is available at `google-auth-oauthlib.readthedocs.io`_.
-        
-        .. _google-auth-oauthlib.readthedocs.io: http://google-auth-oauthlib.readthedocs.io/
-        
-        License
-        -------
-        
-        Apache 2.0 - See `the LICENSE`_ for more information.
-        
-        .. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/master/LICENSE
-        
 Keywords: google auth oauth client oauthlib
-Platform: UNKNOWN
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.6
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
-Classifier: Development Status :: 3 - Alpha
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Operating System :: POSIX
@@ -63,3 +24,55 @@ Classifier: Operating System :: OS Independent
 Classifier: Topic :: Internet :: WWW/HTTP
 Requires-Python: >=3.6
 Provides-Extra: tool
+License-File: LICENSE
+
+oauthlib integration for Google Auth
+====================================
+
+|pypi|
+
+This library provides `oauthlib`_ integration with `google-auth`_.
+
+.. |build| image:: https://travis-ci.org/googleapis/google-auth-library-python-oauthlib.svg?branch=main
+   :target: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
+.. |pypi| image:: https://img.shields.io/pypi/v/google-auth-oauthlib.svg
+   :target: https://pypi.python.org/pypi/google-auth-oauthlib
+
+.. _oauthlib: https://github.com/idan/oauthlib
+.. _google-auth: https://github.com/googleapis/google-auth-library-python
+
+Installing
+----------
+
+You can install using `pip`_::
+
+    $ pip install google-auth-oauthlib
+
+.. _pip: https://pip.pypa.io/en/stable/
+
+Documentation
+-------------
+
+The latest documentation is available at `google-auth-oauthlib.googleapis.dev`_.
+
+.. _google-auth-oauthlib.googleapis.dev: https://googleapis.dev/python/google-auth-oauthlib/latest/index.html
+
+Supported Python Versions
+-------------------------
+Python >= 3.6
+
+
+Unsupported Python Versions
+---------------------------
+
+Python == 2.7, Python == 3.5.
+
+The last version of this library compatible with Python 2.7 and 3.5 is
+`google-auth-oauthlib==0.4.1`.
+
+License
+-------
+
+Apache 2.0 - See `the LICENSE`_ for more information.
+
+.. _the LICENSE: https://github.com/googleapis/google-auth-library-python-oauthlib/blob/main/LICENSE
diff --git a/google_auth_oauthlib.egg-info/entry_points.txt b/google_auth_oauthlib.egg-info/entry_points.txt
index 87cadbc..a33f32f 100644
--- a/google_auth_oauthlib.egg-info/entry_points.txt
+++ b/google_auth_oauthlib.egg-info/entry_points.txt
@@ -1,3 +1,2 @@
 [console_scripts]
 google-oauthlib-tool = google_auth_oauthlib.tool.__main__:main [tool]
-
diff --git a/google_auth_oauthlib.egg-info/requires.txt b/google_auth_oauthlib.egg-info/requires.txt
index 3cf0a97..75df711 100644
--- a/google_auth_oauthlib.egg-info/requires.txt
+++ b/google_auth_oauthlib.egg-info/requires.txt
@@ -1,5 +1,5 @@
-google-auth
+google-auth>=2.15.0
 requests-oauthlib>=0.7.0
 
 [tool]
-click
+click>=6.0.0
diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py
index 8196f9b..a3785e9 100644
--- a/google_auth_oauthlib/flow.py
+++ b/google_auth_oauthlib/flow.py
@@ -15,41 +15,38 @@
 """OAuth 2.0 Authorization Flow
 
 This module provides integration with `requests-oauthlib`_ for running the
-`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.
+`OAuth 2.0 Authorization Flow`_ and acquiring user credentials.  See
+`Using OAuth 2.0 to Access Google APIs`_ for an overview of OAuth 2.0
+authorization scenarios Google APIs support.
 
-Here's an example of using :class:`Flow` with the installed application
-authorization flow::
+Here's an example of using :class:`InstalledAppFlow`::
 
-    from google_auth_oauthlib.flow import Flow
+    from google_auth_oauthlib.flow import InstalledAppFlow
 
     # Create the flow using the client secrets file from the Google API
     # Console.
-    flow = Flow.from_client_secrets_file(
-        'path/to/client_secrets.json',
-        scopes=['profile', 'email'],
-        redirect_uri='urn:ietf:wg:oauth:2.0:oob')
+    flow = InstalledAppFlow.from_client_secrets_file(
+        'client_secrets.json',
+        scopes=['profile', 'email'])
 
-    # Tell the user to go to the authorization URL.
-    auth_url, _ = flow.authorization_url(prompt='consent')
-
-    print('Please go to this URL: {}'.format(auth_url))
-
-    # The user will get an authorization code. This code is used to get the
-    # access token.
-    code = input('Enter the authorization code: ')
-    flow.fetch_token(code=code)
+    flow.run_local_server()
 
     # You can use flow.credentials, or you can just get a requests session
     # using flow.authorized_session.
     session = flow.authorized_session()
-    print(session.get('https://www.googleapis.com/userinfo/v2/me').json())
 
-This particular flow can be handled entirely by using
-:class:`InstalledAppFlow`.
+    profile_info = session.get(
+        'https://www.googleapis.com/userinfo/v2/me').json()
 
-.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
+    print(profile_info)
+    # {'name': '...',  'email': '...', ...}
+
+.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/latest/
 .. _OAuth 2.0 Authorization Flow:
     https://tools.ietf.org/html/rfc6749#section-1.2
+.. _Using OAuth 2.0 to Access Google APIs:
+    https://developers.google.com/identity/protocols/oauth2
+
 """
 from base64 import urlsafe_b64encode
 import hashlib
@@ -67,7 +64,6 @@ import wsgiref.util
 
 import google.auth.transport.requests
 import google.oauth2.credentials
-from six.moves import input
 
 import google_auth_oauthlib.helpers
 
@@ -88,7 +84,7 @@ class Flow(object):
     from the `Google API Console`_.
 
     .. _client secrets file:
-        https://developers.google.com/identity/protocols/OAuth2WebServer
+        https://developers.google.com/identity/protocols/oauth2/web-server
         #creatingcred
     .. _Google API Console:
         https://console.developers.google.com/apis/credentials
@@ -101,7 +97,7 @@ class Flow(object):
         client_config,
         redirect_uri=None,
         code_verifier=None,
-        autogenerate_code_verifier=False,
+        autogenerate_code_verifier=True,
     ):
         """
         Args:
@@ -119,8 +115,8 @@ class Flow(object):
             autogenerate_code_verifier (bool): If true, auto-generate a
                 code_verifier.
         .. _client secrets:
-            https://developers.google.com/api-client-library/python/guide
-            /aaa_client_secrets
+            https://github.com/googleapis/google-api-python-client/blob
+            /main/docs/client-secrets.md
         """
         self.client_type = client_type
         """str: The client type, either ``'web'`` or ``'installed'``"""
@@ -153,8 +149,7 @@ class Flow(object):
                 format.
 
         .. _client secrets:
-            https://developers.google.com/api-client-library/python/guide
-            /aaa_client_secrets
+            https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md
         """
         if "web" in client_config:
             client_type = "web"
@@ -213,6 +208,8 @@ class Flow(object):
 
     @redirect_uri.setter
     def redirect_uri(self, value):
+        """The OAuth 2.0 redirect URI. Pass-through to
+        ``self.oauth2session.redirect_uri``."""
         self.oauth2session.redirect_uri = value
 
     def authorization_url(self, **kwargs):
@@ -327,9 +324,7 @@ class InstalledAppFlow(Flow):
     local development or applications that are installed on a desktop operating
     system.
 
-    This flow has two strategies: The console strategy provided by
-    :meth:`run_console` and the local server strategy provided by
-    :meth:`run_local_server`.
+    This flow uses a local server strategy provided by :meth:`run_local_server`.
 
     Example::
 
@@ -350,19 +345,16 @@ class InstalledAppFlow(Flow):
         # {'name': '...',  'email': '...', ...}
 
 
-    Note that these aren't the only two ways to accomplish the installed
-    application flow, they are just the most common ways. You can use the
+    Note that this isn't the only way to accomplish the installed
+    application flow, just one of the most common. You can use the
     :class:`Flow` class to perform the same flow with different methods of
     presenting the authorization URL to the user or obtaining the authorization
     response, such as using an embedded web view.
 
     .. _Installed Application Authorization Flow:
-        https://developers.google.com/api-client-library/python/auth
-        /installed-app
+        https://github.com/googleapis/google-api-python-client/blob/main/docs/oauth-installed.md
     """
 
-    _OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
-
     _DEFAULT_AUTH_PROMPT_MESSAGE = (
         "Please visit this URL to authorize this application: {url}"
     )
@@ -376,52 +368,16 @@ class InstalledAppFlow(Flow):
         "The authentication flow has completed. You may close this window."
     )
 
-    def run_console(
-        self,
-        authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
-        authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE,
-        **kwargs
-    ):
-        """Run the flow using the console strategy.
-
-        The console strategy instructs the user to open the authorization URL
-        in their browser. Once the authorization is complete the authorization
-        server will give the user a code. The user then must copy & paste this
-        code into the application. The code is then exchanged for a token.
-
-        Args:
-            authorization_prompt_message (str): The message to display to tell
-                the user to navigate to the authorization URL.
-            authorization_code_message (str): The message to display when
-                prompting the user for the authorization code.
-            kwargs: Additional keyword arguments passed through to
-                :meth:`authorization_url`.
-
-        Returns:
-            google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
-                for the user.
-        """
-        kwargs.setdefault("prompt", "consent")
-
-        self.redirect_uri = self._OOB_REDIRECT_URI
-
-        auth_url, _ = self.authorization_url(**kwargs)
-
-        print(authorization_prompt_message.format(url=auth_url))
-
-        code = input(authorization_code_message)
-
-        self.fetch_token(code=code)
-
-        return self.credentials
-
     def run_local_server(
         self,
         host="localhost",
+        bind_addr=None,
         port=8080,
         authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
         success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
         open_browser=True,
+        redirect_uri_trailing_slash=True,
+        timeout_seconds=None,
         **kwargs
     ):
         """Run the flow using the server strategy.
@@ -437,13 +393,25 @@ class InstalledAppFlow(Flow):
         Args:
             host (str): The hostname for the local redirect server. This will
                 be served over http, not https.
+            bind_addr (str): Optionally provide an ip address for the redirect
+                server to listen on when it is not the same as host
+                (e.g. in a container). Default value is None,
+                which means that the redirect server will listen
+                on the ip address specified in the host parameter.
             port (int): The port for the local redirect server.
-            authorization_prompt_message (str): The message to display to tell
-                the user to navigate to the authorization URL.
+            authorization_prompt_message (str | None): The message to display to tell
+                the user to navigate to the authorization URL. If None or empty,
+                don't display anything.
             success_message (str): The message to display in the web browser
                 the authorization flow is complete.
             open_browser (bool): Whether or not to open the authorization URL
                 in the user's browser.
+            redirect_uri_trailing_slash (bool): whether or not to add trailing
+                slash when constructing the redirect_uri. Default value is True.
+            timeout_seconds (int): It will raise an error after the timeout timing
+                if there are no credentials response. The value is in seconds.
+                When set to None there is no timeout.
+                Default value is None.
             kwargs: Additional keyword arguments passed through to
                 :meth:`authorization_url`.
 
@@ -455,17 +423,22 @@ class InstalledAppFlow(Flow):
         # Fail fast if the address is occupied
         wsgiref.simple_server.WSGIServer.allow_reuse_address = False
         local_server = wsgiref.simple_server.make_server(
-            host, port, wsgi_app, handler_class=_WSGIRequestHandler
+            bind_addr or host, port, wsgi_app, handler_class=_WSGIRequestHandler
         )
 
-        self.redirect_uri = "http://{}:{}/".format(host, local_server.server_port)
+        redirect_uri_format = (
+            "http://{}:{}/" if redirect_uri_trailing_slash else "http://{}:{}"
+        )
+        self.redirect_uri = redirect_uri_format.format(host, local_server.server_port)
         auth_url, _ = self.authorization_url(**kwargs)
 
         if open_browser:
             webbrowser.open(auth_url, new=1, autoraise=True)
 
-        print(authorization_prompt_message.format(url=auth_url))
+        if authorization_prompt_message:
+            print(authorization_prompt_message.format(url=auth_url))
 
+        local_server.timeout = timeout_seconds
         local_server.handle_request()
 
         # Note: using https here because oauthlib is very picky that
@@ -517,6 +490,6 @@ class _RedirectWSGIApp(object):
         Returns:
             Iterable[bytes]: The response body.
         """
-        start_response("200 OK", [("Content-type", "text/plain")])
+        start_response("200 OK", [("Content-type", "text/plain; charset=utf-8")])
         self.last_request_uri = wsgiref.util.request_uri(environ)
         return [self._success_message.encode("utf-8")]
diff --git a/google_auth_oauthlib/helpers.py b/google_auth_oauthlib/helpers.py
index 49a39e4..25462f4 100644
--- a/google_auth_oauthlib/helpers.py
+++ b/google_auth_oauthlib/helpers.py
@@ -18,12 +18,13 @@ This module provides helpers for integrating with `requests-oauthlib`_.
 Typically, you'll want to use the higher-level helpers in
 :mod:`google_auth_oauthlib.flow`.
 
-.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/stable/
+.. _requests-oauthlib: http://requests-oauthlib.readthedocs.io/en/latest/
 """
 
 import datetime
 import json
 
+from google.auth import external_account_authorized_user
 import google.oauth2.credentials
 import requests_oauthlib
 
@@ -51,8 +52,7 @@ def session_from_client_config(client_config, scopes, **kwargs):
             oauthlib session and the validated client configuration.
 
     .. _client secrets:
-        https://developers.google.com/api-client-library/python/guide
-        /aaa_client_secrets
+        https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md
     """
 
     if "web" in client_config:
@@ -89,8 +89,7 @@ def session_from_client_secrets_file(client_secrets_file, scopes, **kwargs):
             oauthlib session and the validated client configuration.
 
     .. _client secrets:
-        https://developers.google.com/api-client-library/python/guide
-        /aaa_client_secrets
+        https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md
     """
     with open(client_secrets_file, "r") as json_file:
         client_config = json.load(json_file)
@@ -127,14 +126,26 @@ def credentials_from_session(session, client_config=None):
             "There is no access token for this session, did you call " "fetch_token?"
         )
 
-    credentials = google.oauth2.credentials.Credentials(
-        session.token["access_token"],
-        refresh_token=session.token.get("refresh_token"),
-        id_token=session.token.get("id_token"),
-        token_uri=client_config.get("token_uri"),
-        client_id=client_config.get("client_id"),
-        client_secret=client_config.get("client_secret"),
-        scopes=session.scope,
-    )
+    if "3pi" in client_config:
+        credentials = external_account_authorized_user.Credentials(
+            token=session.token["access_token"],
+            refresh_token=session.token.get("refresh_token"),
+            token_url=client_config.get("token_uri"),
+            client_id=client_config.get("client_id"),
+            client_secret=client_config.get("client_secret"),
+            token_info_url=client_config.get("token_info_url"),
+            scopes=session.scope,
+        )
+    else:
+        credentials = google.oauth2.credentials.Credentials(
+            session.token["access_token"],
+            refresh_token=session.token.get("refresh_token"),
+            id_token=session.token.get("id_token"),
+            token_uri=client_config.get("token_uri"),
+            client_id=client_config.get("client_id"),
+            client_secret=client_config.get("client_secret"),
+            scopes=session.scope,
+            granted_scopes=session.token.get("scope"),
+        )
     credentials.expiry = datetime.datetime.utcfromtimestamp(session.token["expires_at"])
     return credentials
diff --git a/google_auth_oauthlib/interactive.py b/google_auth_oauthlib/interactive.py
index c6a5d28..b1ed990 100644
--- a/google_auth_oauthlib/interactive.py
+++ b/google_auth_oauthlib/interactive.py
@@ -21,10 +21,68 @@ notebooks.
 
 from __future__ import absolute_import
 
+import contextlib
+import socket
+
 import google_auth_oauthlib.flow
 
 
-def get_user_credentials(scopes, client_id, client_secret):
+LOCALHOST = "localhost"
+DEFAULT_PORTS_TO_TRY = 100
+
+
+def is_port_open(port):
+    """Check if a port is open on localhost.
+    Based on StackOverflow answer: https://stackoverflow.com/a/43238489/101923
+    Parameters
+    ----------
+    port : int
+        A port to check on localhost.
+    Returns
+    -------
+    is_open : bool
+        True if a socket can be opened at the requested port.
+    """
+    with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
+        try:
+            sock.bind((LOCALHOST, port))
+            sock.listen(1)
+        except socket.error:
+            is_open = False
+        else:
+            is_open = True
+    return is_open
+
+
+def find_open_port(start=8080, stop=None):
+    """Find an open port between ``start`` and ``stop``.
+    Parameters
+    ----------
+    start : Optional[int]
+        Beginning of range of ports to try. Defaults to 8080.
+    stop : Optional[int]
+        End of range of ports to try (not including exactly equals ``stop``).
+        This function tries 100 possible ports if no ``stop`` is specified.
+    Returns
+    -------
+    Optional[int]
+        ``None`` if no open port is found, otherwise an integer indicating an
+        open port.
+    """
+    if not stop:
+        stop = start + DEFAULT_PORTS_TO_TRY
+
+    for port in range(start, stop):
+        if is_port_open(port):
+            return port
+
+    # No open ports found.
+    return None
+
+
+def get_user_credentials(
+    scopes, client_id, client_secret, minimum_port=8080, maximum_port=None
+):
     """Gets credentials associated with your Google user account.
 
     This function authenticates using your user credentials by going through
@@ -53,6 +111,12 @@ def get_user_credentials(scopes, client_id, client_secret):
             A string that verifies your application to Google APIs. Find this
             value in the `Credentials page on the Google Developer's Console
             <https://console.developers.google.com/apis/credentials>`_.
+        minimum_port (int):
+            Beginning of range of ports to try for redirect URI HTTP server.
+            Defaults to 8080.
+        maximum_port (Optional[int]):
+            End of range of ports to try (not including exactly equals ``stop``).
+            This function tries 100 possible ports if no ``stop`` is specified.
 
     Returns:
         google.oauth2.credentials.Credentials:
@@ -92,7 +156,6 @@ def get_user_credentials(scopes, client_id, client_secret):
         "installed": {
             "client_id": client_id,
             "client_secret": client_secret,
-            "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob"],
             "auth_uri": "https://accounts.google.com/o/oauth2/auth",
             "token_uri": "https://oauth2.googleapis.com/token",
         }
@@ -102,4 +165,8 @@ def get_user_credentials(scopes, client_id, client_secret):
         client_config, scopes=scopes
     )
 
-    return app_flow.run_console()
+    port = find_open_port(start=minimum_port, stop=maximum_port)
+    if not port:
+        raise ConnectionError("Could not find open port.")
+
+    return app_flow.run_local_server(host=LOCALHOST, port=port)
diff --git a/google_auth_oauthlib/tool/__main__.py b/google_auth_oauthlib/tool/__main__.py
index f13f101..db679a1 100644
--- a/google_auth_oauthlib/tool/__main__.py
+++ b/google_auth_oauthlib/tool/__main__.py
@@ -72,15 +72,7 @@ DEFAULT_CREDENTIALS_FILENAME = "credentials.json"
     default=os.path.join(click.get_app_dir(APP_NAME), DEFAULT_CREDENTIALS_FILENAME),
     help="Path to store OAuth2 credentials.",
 )
-@click.option(
-    "--headless",
-    is_flag=True,
-    metavar="<headless_mode>",
-    show_default=True,
-    default=False,
-    help="Run a console based flow.",
-)
-def main(client_secrets, scope, save, credentials, headless):
+def main(client_secrets, scope, save, credentials):
     """Command-line tool for obtaining authorization and credentials from a user.
 
     This tool uses the OAuth 2.0 Authorization Code grant as described
@@ -88,9 +80,7 @@ def main(client_secrets, scope, save, credentials, headless):
     https://tools.ietf.org/html/rfc6749#section-1.3.1
 
     This tool is intended for assist developers in obtaining credentials
-    for testing applications where it may not be possible or easy to run a
-    complete OAuth 2.0 authorization flow, especially in the case of code
-    samples or embedded devices without input / display capabilities.
+    for testing applications or samples.
 
     This is not intended for production use where a combination of
     companion and on-device applications should complete the OAuth 2.0
@@ -102,10 +92,7 @@ def main(client_secrets, scope, save, credentials, headless):
         client_secrets, scopes=scope
     )
 
-    if not headless:
-        creds = flow.run_local_server()
-    else:
-        creds = flow.run_console()
+    creds = flow.run_local_server()
 
     creds_data = {
         "token": creds.token,
diff --git a/setup.py b/setup.py
index 8b8e707..f305c06 100644
--- a/setup.py
+++ b/setup.py
@@ -18,16 +18,16 @@ from setuptools import find_packages
 from setuptools import setup
 
 
-TOOL_DEPENDENCIES = "click"
+TOOL_DEPENDENCIES = "click>=6.0.0"
 
-DEPENDENCIES = ("google-auth", "requests-oauthlib>=0.7.0")
+DEPENDENCIES = ("google-auth>=2.15.0", "requests-oauthlib>=0.7.0")
 
 
 with io.open("README.rst", "r") as fh:
     long_description = fh.read()
 
 
-version = "0.4.2"
+version = "1.0.0"
 
 setup(
     name="google-auth-oauthlib",
@@ -54,7 +54,9 @@ setup(
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
-        "Development Status :: 3 - Alpha",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Development Status :: 5 - Production/Stable",
         "Intended Audience :: Developers",
         "License :: OSI Approved :: Apache Software License",
         "Operating System :: POSIX",
diff --git a/tests/unit/data/client_secrets.json b/tests/unit/data/client_secrets.json
index 1baa499..f1ff8af 100644
--- a/tests/unit/data/client_secrets.json
+++ b/tests/unit/data/client_secrets.json
@@ -7,7 +7,6 @@
     "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
     "client_secret": "itsasecrettoeveryone",
     "redirect_uris": [
-      "urn:ietf:wg:oauth:2.0:oob",
       "http://localhost"
     ]
   }
diff --git a/tests/unit/test_flow.py b/tests/unit/test_flow.py
index ea9afd9..d9c9f8b 100644
--- a/tests/unit/test_flow.py
+++ b/tests/unit/test_flow.py
@@ -24,7 +24,7 @@ import socket
 import mock
 import pytest
 import requests
-from six.moves import urllib
+import urllib
 
 from google_auth_oauthlib import flow
 
@@ -282,23 +282,6 @@ class TestInstalledAppFlow(object):
         with fetch_token_patch as fetch_token_mock:
             yield fetch_token_mock
 
-    @mock.patch("google_auth_oauthlib.flow.input", autospec=True)
-    def test_run_console(self, input_mock, instance, mock_fetch_token):
-        input_mock.return_value = mock.sentinel.code
-        instance.code_verifier = "amanaplanacanalpanama"
-        credentials = instance.run_console()
-
-        assert credentials.token == mock.sentinel.access_token
-        assert credentials._refresh_token == mock.sentinel.refresh_token
-        assert credentials.id_token == mock.sentinel.id_token
-
-        mock_fetch_token.assert_called_with(
-            CLIENT_SECRETS_INFO["web"]["token_uri"],
-            client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"],
-            code=mock.sentinel.code,
-            code_verifier="amanaplanacanalpanama",
-        )
-
     @pytest.mark.webtest
     @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
     def test_run_local_server(self, webbrowser_mock, instance, mock_fetch_token, port):
@@ -321,6 +304,7 @@ class TestInstalledAppFlow(object):
         assert credentials._refresh_token == mock.sentinel.refresh_token
         assert credentials.id_token == mock.sentinel.id_token
         assert webbrowser_mock.open.called
+        assert instance.redirect_uri == f"http://localhost:{port}/"
 
         expected_auth_response = auth_redirect_url.replace("http", "https")
         mock_fetch_token.assert_called_with(
@@ -341,7 +325,13 @@ class TestInstalledAppFlow(object):
         instance.code_verifier = "amanaplanacanalpanama"
 
         with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
-            future = pool.submit(partial(instance.run_local_server, port=port))
+            future = pool.submit(
+                partial(
+                    instance.run_local_server,
+                    port=port,
+                    redirect_uri_trailing_slash=False,
+                )
+            )
 
             while not future.done():
                 try:
@@ -355,6 +345,7 @@ class TestInstalledAppFlow(object):
         assert credentials._refresh_token == mock.sentinel.refresh_token
         assert credentials.id_token == mock.sentinel.id_token
         assert webbrowser_mock.open.called
+        assert instance.redirect_uri == f"http://localhost:{port}"
 
         expected_auth_response = auth_redirect_url.replace("http", "https")
         mock_fetch_token.assert_called_with(
@@ -379,6 +370,24 @@ class TestInstalledAppFlow(object):
 
         assert not webbrowser_mock.open.called
 
+    @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
+    @mock.patch("wsgiref.simple_server.make_server", autospec=True)
+    def test_run_local_server_bind_addr(
+        self, make_server_mock, webbrowser_mock, instance, mock_fetch_token
+    ):
+        def assign_last_request_uri(host, port, wsgi_app, **kwargs):
+            wsgi_app.last_request_uri = self.REDIRECT_REQUEST_PATH
+            return mock.Mock()
+
+        make_server_mock.side_effect = assign_last_request_uri
+
+        my_ip = socket.gethostbyname(socket.gethostname())
+        instance.run_local_server(bind_addr=my_ip, host="localhost")
+
+        assert webbrowser_mock.open.called
+        name, args, kwargs = make_server_mock.mock_calls[0]
+        assert args[0] == my_ip
+
     @pytest.mark.webtest
     @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
     def test_run_local_server_occupied_port(
diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py
index 9b6472c..9df49de 100644
--- a/tests/unit/test_helpers.py
+++ b/tests/unit/test_helpers.py
@@ -19,6 +19,8 @@ import os
 import mock
 import pytest
 
+from google.auth import external_account_authorized_user
+import google.oauth2.credentials
 from google_auth_oauthlib import helpers
 
 DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
@@ -85,6 +87,7 @@ def test_credentials_from_session(session):
 
     credentials = helpers.credentials_from_session(session, CLIENT_SECRETS_INFO["web"])
 
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
     assert credentials.token == mock.sentinel.access_token
     assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
     assert credentials._refresh_token == mock.sentinel.refresh_token
@@ -92,6 +95,60 @@ def test_credentials_from_session(session):
     assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
     assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"]
     assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"]
+    assert credentials.scopes == session.scope
+    assert credentials.granted_scopes is None
+
+
+def test_credentials_from_session_granted_scopes(session):
+    granted_scopes = ["scope1", "scope2"]
+    session.token = {
+        "access_token": mock.sentinel.access_token,
+        "refresh_token": mock.sentinel.refresh_token,
+        "id_token": mock.sentinel.id_token,
+        "expires_at": 643969200.0,
+        "scope": granted_scopes,
+    }
+
+    credentials = helpers.credentials_from_session(session, CLIENT_SECRETS_INFO["web"])
+
+    assert isinstance(credentials, google.oauth2.credentials.Credentials)
+    assert credentials.token == mock.sentinel.access_token
+    assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
+    assert credentials._refresh_token == mock.sentinel.refresh_token
+    assert credentials.id_token == mock.sentinel.id_token
+    assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
+    assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"]
+    assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"]
+    assert credentials.scopes == session.scope
+    assert credentials.granted_scopes == granted_scopes
+
+
+def test_credentials_from_session_3pi(session):
+    session.token = {
+        "access_token": mock.sentinel.access_token,
+        "refresh_token": mock.sentinel.refresh_token,
+        "id_token": mock.sentinel.id_token,
+        "expires_at": 643969200.0,
+    }
+
+    client_secrets_info = CLIENT_SECRETS_INFO["web"].copy()
+    client_secrets_info["3pi"] = True
+    client_secrets_info[
+        "token_info_url"
+    ] = "https://accounts.google.com/o/oauth2/introspect"
+    credentials = helpers.credentials_from_session(session, client_secrets_info)
+
+    assert isinstance(credentials, external_account_authorized_user.Credentials)
+    assert credentials.token == mock.sentinel.access_token
+    assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
+    assert credentials._refresh_token == mock.sentinel.refresh_token
+    assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
+    assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"]
+    assert credentials._token_url == CLIENT_SECRETS_INFO["web"]["token_uri"]
+    assert (
+        credentials._token_info_url == "https://accounts.google.com/o/oauth2/introspect"
+    )
+    assert credentials.scopes == session.scope
 
 
 def test_bad_credentials(session):
diff --git a/tests/unit/test_interactive.py b/tests/unit/test_interactive.py
index c6a8f97..3a354eb 100644
--- a/tests/unit/test_interactive.py
+++ b/tests/unit/test_interactive.py
@@ -12,7 +12,47 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import socket
+
 import mock
+import pytest
+
+
+def test_find_open_port_finds_start_port(monkeypatch):
+    from google_auth_oauthlib import interactive as module_under_test
+
+    monkeypatch.setattr(socket, "socket", mock.create_autospec(socket.socket))
+    port = module_under_test.find_open_port(9999)
+    assert port == 9999
+
+
+def test_find_open_port_finds_stop_port(monkeypatch):
+    from google_auth_oauthlib import interactive as module_under_test
+
+    socket_instance = mock.create_autospec(socket.socket, instance=True)
+
+    def mock_socket(family, type_):
+        return socket_instance
+
+    monkeypatch.setattr(socket, "socket", mock_socket)
+    socket_instance.listen.side_effect = [socket.error] * 99 + [None]
+    port = module_under_test.find_open_port(9000, stop=9100)
+    assert port == 9099
+
+
+def test_find_open_port_returns_none(monkeypatch):
+    from google_auth_oauthlib import interactive as module_under_test
+
+    socket_instance = mock.create_autospec(socket.socket, instance=True)
+
+    def mock_socket(family, type_):
+        return socket_instance
+
+    monkeypatch.setattr(socket, "socket", mock_socket)
+    socket_instance.listen.side_effect = socket.error
+    port = module_under_test.find_open_port(9000)
+    assert port is None
+    socket_instance.listen.assert_has_calls(mock.call(1) for _ in range(100))
 
 
 def test_get_user_credentials():
@@ -33,4 +73,25 @@ def test_get_user_credentials():
     actual_client_config = mock_flow.from_client_config.call_args[0][0]
     assert actual_client_config["installed"]["client_id"] == "some-client-id"
     assert actual_client_config["installed"]["client_secret"] == "shh-secret"
-    mock_flow_instance.run_console.assert_called_once()
+    mock_flow_instance.run_local_server.assert_called_once()
+
+
+def test_get_user_credentials_raises_connectionerror(monkeypatch):
+    from google_auth_oauthlib import flow
+    from google_auth_oauthlib import interactive as module_under_test
+
+    def mock_find_open_port(start=8080, stop=None):
+        return None
+
+    monkeypatch.setattr(module_under_test, "find_open_port", mock_find_open_port)
+    mock_flow = mock.create_autospec(flow.InstalledAppFlow, instance=True)
+
+    with mock.patch(
+        "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True
+    ) as mock_flow, pytest.raises(ConnectionError):
+        mock_flow.from_client_config.return_value = mock_flow
+        module_under_test.get_user_credentials(
+            ["scopes"], "some-client-id", "shh-secret"
+        )
+
+    mock_flow.run_local_server.assert_not_called()
diff --git a/tests/unit/test_tool.py b/tests/unit/test_tool.py
index 3eeb5c5..d76ee53 100644
--- a/tests/unit/test_tool.py
+++ b/tests/unit/test_tool.py
@@ -57,16 +57,6 @@ class TestMain(object):
             flow.return_value = dummy_credentials
             yield flow
 
-    @pytest.fixture
-    def console_mock(self, dummy_credentials):
-        run_console_patch = mock.patch.object(
-            google_auth_oauthlib.flow.InstalledAppFlow, "run_console", autospec=True
-        )
-
-        with run_console_patch as flow:
-            flow.return_value = dummy_credentials
-            yield flow
-
     def test_help(self, runner):
         result = runner.invoke(cli.main, ["--help"])
         assert not result.exception
@@ -91,22 +81,6 @@ class TestMain(object):
         assert creds.client_secret == dummy_credentials.client_secret
         assert creds.scopes == dummy_credentials.scopes
 
-    def test_headless(self, runner, dummy_credentials, console_mock):
-        result = runner.invoke(
-            cli.main,
-            [
-                "--client-secrets",
-                CLIENT_SECRETS_FILE,
-                "--scope",
-                "somescope",
-                "--headless",
-            ],
-        )
-        console_mock.assert_called_with(mock.ANY)
-        assert not result.exception
-        assert dummy_credentials.refresh_token in result.output
-        assert result.exit_code == 0
-
     def test_save_new_dir(self, runner, dummy_credentials, local_server_mock):
         credentials_tmpdir = tempfile.mkdtemp()
         credentials_path = os.path.join(

More details

Full run details

Historical runs