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(