././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/0002755000000000017530000000000014536115377013422 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/LICENSE0000664000000000017530000002613614536115127014430 0ustar00root Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/MANIFEST.in0000664000000000017530000000153414536115127015154 0ustar00root# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # 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 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Generated by synthtool. DO NOT EDIT! include README.rst LICENSE 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/PKG-INFO0000644000000000017530000000520414536115377014516 0ustar00rootMetadata-Version: 2.1 Name: google-auth-oauthlib Version: 1.2.0 Summary: Google Authentication Library Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib Author: Google Cloud Platform Author-email: googleapis-packages@google.com License: Apache 2.0 Keywords: google auth oauth client oauthlib 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: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Requires-Python: >=3.6 License-File: LICENSE Requires-Dist: google-auth>=2.15.0 Requires-Dist: requests-oauthlib>=0.7.0 Provides-Extra: tool Requires-Dist: click>=6.0.0; extra == "tool" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/README.rst0000664000000000017530000000255014536115127015104 0ustar00rootoauthlib 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3222244 google-auth-oauthlib-1.2.0/docs/0002755000000000017530000000000014536115377014352 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/docs/conf.py0000664000000000017530000003020514536115127015642 0ustar00root# -*- coding: utf-8 -*- # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # google-auth-oauthlib documentation build configuration file # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # For plugins that can not read conf.py. # See also: https://github.com/docascode/sphinx-docfx-yaml/issues/85 sys.path.insert(0, os.path.abspath(".")) __version__ = "" # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = "1.5.5" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.napoleon", "sphinx.ext.todo", "sphinx.ext.viewcode", "recommonmark", ] # autodoc/autosummary flags autoclass_content = "both" autodoc_default_options = {"members": True} autosummary_generate = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = [".rst", ".md"] # The encoding of source files. # source_encoding = 'utf-8-sig' # The root toctree document. root_doc = "index" # General information about the project. project = "google-auth-oauthlib" copyright = "2019, Google" author = "Google APIs" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release = __version__ # The short X.Y version. version = ".".join(release.split(".")[0:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [ "_build", "**/.nox/**/*", "samples/AUTHORING_GUIDE.md", "samples/CONTRIBUTING.md", "samples/snippets/README.rst", ] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "description": "Google Cloud Client Libraries for google-auth-oauthlib", "github_user": "googleapis", "github_repo": "google-auth-library-python-oauthlib", "github_banner": True, "font_family": "'Roboto', Georgia, sans", "head_font_family": "'Roboto', Georgia, serif", "code_font_family": "'Roboto Mono', 'Consolas', monospace", } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = "google-auth-oauthlib-doc" # -- Options for warnings ------------------------------------------------------ suppress_warnings = [ # Temporarily suppress this to avoid "more than one target found for # cross-reference" warning, which are intractable for us to avoid while in # a mono-repo. # See https://github.com/sphinx-doc/sphinx/blob # /2a65ffeef5c107c19084fabdd706cdff3f52d93c/sphinx/domains/python.py#L843 "ref.python" ] # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( root_doc, "google-auth-oauthlib.tex", "google-auth-oauthlib Documentation", author, "manual", ) ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( root_doc, "google-auth-oauthlib", "google-auth-oauthlib Documentation", [author], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( root_doc, "google-auth-oauthlib", "google-auth-oauthlib Documentation", author, "google-auth-oauthlib", "google-auth-oauthlib Library", "APIs", ) ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://python.readthedocs.org/en/latest/", None), "google-auth": ("https://googleapis.dev/python/google-auth/latest/", None), "google.api_core": ( "https://googleapis.dev/python/google-api-core/latest/", None, ), "grpc": ("https://grpc.github.io/grpc/python/", None), "proto-plus": ("https://proto-plus-python.readthedocs.io/en/latest/", None), "protobuf": ("https://googleapis.dev/python/protobuf/latest/", None), } # Napoleon settings napoleon_google_docstring = True napoleon_numpy_docstring = True napoleon_include_private_with_doc = False napoleon_include_special_with_doc = True napoleon_use_admonition_for_examples = False napoleon_use_admonition_for_notes = False napoleon_use_admonition_for_references = False napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3222244 google-auth-oauthlib-1.2.0/google_auth_oauthlib/0002755000000000017530000000000014536115377017606 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/__init__.py0000664000000000017530000000151614536115127021713 0ustar00root# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # 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 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """oauthlib integration for Google Auth This library provides `oauthlib `__ integration with `google-auth `__. """ from .interactive import get_user_credentials __all__ = ["get_user_credentials"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/flow.py0000664000000000017530000004623014536115127021125 0ustar00root# Copyright 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """OAuth 2.0 Authorization Flow This module provides integration with `requests-oauthlib`_ for running the `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:`InstalledAppFlow`:: from google_auth_oauthlib.flow import InstalledAppFlow # Create the flow using the client secrets file from the Google API # Console. flow = InstalledAppFlow.from_client_secrets_file( 'client_secrets.json', scopes=['profile', 'email']) 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() profile_info = session.get( 'https://www.googleapis.com/userinfo/v2/me').json() 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 import json import logging try: from secrets import SystemRandom except ImportError: # pragma: NO COVER from random import SystemRandom from string import ascii_letters, digits import webbrowser import wsgiref.simple_server import wsgiref.util import google.auth.transport.requests import google.oauth2.credentials import google_auth_oauthlib.helpers _LOGGER = logging.getLogger(__name__) class Flow(object): """OAuth 2.0 Authorization Flow This class uses a :class:`requests_oauthlib.OAuth2Session` instance at :attr:`oauth2session` to perform all of the OAuth 2.0 logic. This class just provides convenience methods and sane defaults for doing Google's particular flavors of OAuth 2.0. Typically you'll construct an instance of this flow using :meth:`from_client_secrets_file` and a `client secrets file`_ obtained from the `Google API Console`_. .. _client secrets file: https://developers.google.com/identity/protocols/oauth2/web-server #creatingcred .. _Google API Console: https://console.developers.google.com/apis/credentials """ def __init__( self, oauth2session, client_type, client_config, redirect_uri=None, code_verifier=None, autogenerate_code_verifier=True, ): """ Args: oauth2session (requests_oauthlib.OAuth2Session): The OAuth 2.0 session from ``requests-oauthlib``. client_type (str): The client type, either ``web`` or ``installed``. client_config (Mapping[str, Any]): The client configuration in the Google `client secrets`_ format. redirect_uri (str): The OAuth 2.0 redirect URI if known at flow creation time. Otherwise, it will need to be set using :attr:`redirect_uri`. code_verifier (str): random string of 43-128 chars used to verify the key exchange.using PKCE. autogenerate_code_verifier (bool): If true, auto-generate a code_verifier. .. _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'``""" self.client_config = client_config[client_type] """Mapping[str, Any]: The OAuth 2.0 client configuration.""" self.oauth2session = oauth2session """requests_oauthlib.OAuth2Session: The OAuth 2.0 session.""" self.redirect_uri = redirect_uri self.code_verifier = code_verifier self.autogenerate_code_verifier = autogenerate_code_verifier @classmethod def from_client_config(cls, client_config, scopes, **kwargs): """Creates a :class:`requests_oauthlib.OAuth2Session` from client configuration loaded from a Google-format client secrets file. Args: client_config (Mapping[str, Any]): The client configuration in the Google `client secrets`_ format. scopes (Sequence[str]): The list of scopes to request during the flow. kwargs: Any additional parameters passed to :class:`requests_oauthlib.OAuth2Session` Returns: Flow: The constructed Flow instance. Raises: ValueError: If the client configuration is not in the correct format. .. _client secrets: https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md """ if "web" in client_config: client_type = "web" elif "installed" in client_config: client_type = "installed" else: raise ValueError("Client secrets must be for a web or installed app.") # these args cannot be passed to requests_oauthlib.OAuth2Session code_verifier = kwargs.pop("code_verifier", None) autogenerate_code_verifier = kwargs.pop("autogenerate_code_verifier", None) ( session, client_config, ) = google_auth_oauthlib.helpers.session_from_client_config( client_config, scopes, **kwargs ) redirect_uri = kwargs.get("redirect_uri", None) return cls( session, client_type, client_config, redirect_uri, code_verifier, autogenerate_code_verifier, ) @classmethod def from_client_secrets_file(cls, client_secrets_file, scopes, **kwargs): """Creates a :class:`Flow` instance from a Google client secrets file. Args: client_secrets_file (str): The path to the client secrets .json file. scopes (Sequence[str]): The list of scopes to request during the flow. kwargs: Any additional parameters passed to :class:`requests_oauthlib.OAuth2Session` Returns: Flow: The constructed Flow instance. """ with open(client_secrets_file, "r") as json_file: client_config = json.load(json_file) return cls.from_client_config(client_config, scopes=scopes, **kwargs) @property def redirect_uri(self): """The OAuth 2.0 redirect URI. Pass-through to ``self.oauth2session.redirect_uri``.""" return self.oauth2session.redirect_uri @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): """Generates an authorization URL. This is the first step in the OAuth 2.0 Authorization Flow. The user's browser should be redirected to the returned URL. This method calls :meth:`requests_oauthlib.OAuth2Session.authorization_url` and specifies the client configuration's authorization URI (usually Google's authorization server) and specifies that "offline" access is desired. This is required in order to obtain a refresh token. Args: kwargs: Additional arguments passed through to :meth:`requests_oauthlib.OAuth2Session.authorization_url` Returns: Tuple[str, str]: The generated authorization URL and state. The user must visit the URL to complete the flow. The state is used when completing the flow to verify that the request originated from your application. If your application is using a different :class:`Flow` instance to obtain the token, you will need to specify the ``state`` when constructing the :class:`Flow`. """ kwargs.setdefault("access_type", "offline") if self.autogenerate_code_verifier: chars = ascii_letters + digits + "-._~" rnd = SystemRandom() random_verifier = [rnd.choice(chars) for _ in range(0, 128)] self.code_verifier = "".join(random_verifier) if self.code_verifier: code_hash = hashlib.sha256() code_hash.update(str.encode(self.code_verifier)) unencoded_challenge = code_hash.digest() b64_challenge = urlsafe_b64encode(unencoded_challenge) code_challenge = b64_challenge.decode().split("=")[0] kwargs.setdefault("code_challenge", code_challenge) kwargs.setdefault("code_challenge_method", "S256") url, state = self.oauth2session.authorization_url( self.client_config["auth_uri"], **kwargs ) return url, state def fetch_token(self, **kwargs): """Completes the Authorization Flow and obtains an access token. This is the final step in the OAuth 2.0 Authorization Flow. This is called after the user consents. This method calls :meth:`requests_oauthlib.OAuth2Session.fetch_token` and specifies the client configuration's token URI (usually Google's token server). Args: kwargs: Arguments passed through to :meth:`requests_oauthlib.OAuth2Session.fetch_token`. At least one of ``code`` or ``authorization_response`` must be specified. Returns: Mapping[str, str]: The obtained tokens. Typically, you will not use return value of this function and instead use :meth:`credentials` to obtain a :class:`~google.auth.credentials.Credentials` instance. """ kwargs.setdefault("client_secret", self.client_config["client_secret"]) kwargs.setdefault("code_verifier", self.code_verifier) return self.oauth2session.fetch_token(self.client_config["token_uri"], **kwargs) @property def credentials(self): """Returns credentials from the OAuth 2.0 session. :meth:`fetch_token` must be called before accessing this. This method constructs a :class:`google.oauth2.credentials.Credentials` class using the session's token and the client config. Returns: google.oauth2.credentials.Credentials: The constructed credentials. Raises: ValueError: If there is no access token in the session. """ return google_auth_oauthlib.helpers.credentials_from_session( self.oauth2session, self.client_config ) def authorized_session(self): """Returns a :class:`requests.Session` authorized with credentials. :meth:`fetch_token` must be called before this method. This method constructs a :class:`google.auth.transport.requests.AuthorizedSession` class using this flow's :attr:`credentials`. Returns: google.auth.transport.requests.AuthorizedSession: The constructed session. """ return google.auth.transport.requests.AuthorizedSession(self.credentials) class InstalledAppFlow(Flow): """Authorization flow helper for installed applications. This :class:`Flow` subclass makes it easier to perform the `Installed Application Authorization Flow`_. This flow is useful for local development or applications that are installed on a desktop operating system. This flow uses a local server strategy provided by :meth:`run_local_server`. Example:: from google_auth_oauthlib.flow import InstalledAppFlow flow = InstalledAppFlow.from_client_secrets_file( 'client_secrets.json', scopes=['profile', 'email']) flow.run_local_server() session = flow.authorized_session() profile_info = session.get( 'https://www.googleapis.com/userinfo/v2/me').json() print(profile_info) # {'name': '...', 'email': '...', ...} 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://github.com/googleapis/google-api-python-client/blob/main/docs/oauth-installed.md """ _DEFAULT_AUTH_PROMPT_MESSAGE = ( "Please visit this URL to authorize this application: {url}" ) """str: The message to display when prompting the user for authorization.""" _DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: " """str: The message to display when prompting the user for the authorization code. Used only by the console strategy.""" _DEFAULT_WEB_SUCCESS_MESSAGE = ( "The authentication flow has completed. You may close this window." ) 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, token_audience=None, browser=None, **kwargs ): """Run the flow using the server strategy. The server strategy instructs the user to open the authorization URL in their browser and will attempt to automatically open the URL for them. It will start a local web server to listen for the authorization response. Once authorization is complete the authorization server will redirect the user's browser to the local web server. The web server will get the authorization code from the response and shutdown. The code is then exchanged for a token. 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 | 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. token_audience (str): Passed along with the request for an access token. Determines the endpoints with which the token can be used. Optional. browser (str): specify which browser to open for authentication. If not specified this defaults to default browser. kwargs: Additional keyword arguments passed through to :meth:`authorization_url`. Returns: google.oauth2.credentials.Credentials: The OAuth 2.0 credentials for the user. """ wsgi_app = _RedirectWSGIApp(success_message) # Fail fast if the address is occupied wsgiref.simple_server.WSGIServer.allow_reuse_address = False local_server = wsgiref.simple_server.make_server( bind_addr or host, port, wsgi_app, handler_class=_WSGIRequestHandler ) 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: # if browser is None it defaults to default browser webbrowser.get(browser).open(auth_url, new=1, autoraise=True) 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 # OAuth 2.0 should only occur over https. authorization_response = wsgi_app.last_request_uri.replace("http", "https") self.fetch_token( authorization_response=authorization_response, audience=token_audience ) # This closes the socket local_server.server_close() return self.credentials class _WSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler): """Custom WSGIRequestHandler. Uses a named logger instead of printing to stderr. """ def log_message(self, format, *args): # pylint: disable=redefined-builtin # (format is the argument name defined in the superclass.) _LOGGER.info(format, *args) class _RedirectWSGIApp(object): """WSGI app to handle the authorization redirect. Stores the request URI and displays the given success message. """ def __init__(self, success_message): """ Args: success_message (str): The message to display in the web browser the authorization flow is complete. """ self.last_request_uri = None self._success_message = success_message def __call__(self, environ, start_response): """WSGI Callable. Args: environ (Mapping[str, Any]): The WSGI environment. start_response (Callable[str, list]): The WSGI start_response callable. Returns: Iterable[bytes]: The response body. """ 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")] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/helpers.py0000664000000000017530000001314114536115127021613 0ustar00root# Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Integration helpers. 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/latest/ """ import datetime import json from google.auth import external_account_authorized_user import google.oauth2.credentials import requests_oauthlib _REQUIRED_CONFIG_KEYS = frozenset(("auth_uri", "token_uri", "client_id")) def session_from_client_config(client_config, scopes, **kwargs): """Creates a :class:`requests_oauthlib.OAuth2Session` from client configuration loaded from a Google-format client secrets file. Args: client_config (Mapping[str, Any]): The client configuration in the Google `client secrets`_ format. scopes (Sequence[str]): The list of scopes to request during the flow. kwargs: Any additional parameters passed to :class:`requests_oauthlib.OAuth2Session` Raises: ValueError: If the client configuration is not in the correct format. Returns: Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new oauthlib session and the validated client configuration. .. _client secrets: https://github.com/googleapis/google-api-python-client/blob/main/docs/client-secrets.md """ if "web" in client_config: config = client_config["web"] elif "installed" in client_config: config = client_config["installed"] else: raise ValueError("Client secrets must be for a web or installed app.") if not _REQUIRED_CONFIG_KEYS.issubset(config.keys()): raise ValueError("Client secrets is not in the correct format.") session = requests_oauthlib.OAuth2Session( client_id=config["client_id"], scope=scopes, **kwargs ) return session, client_config def session_from_client_secrets_file(client_secrets_file, scopes, **kwargs): """Creates a :class:`requests_oauthlib.OAuth2Session` instance from a Google-format client secrets file. Args: client_secrets_file (str): The path to the `client secrets`_ .json file. scopes (Sequence[str]): The list of scopes to request during the flow. kwargs: Any additional parameters passed to :class:`requests_oauthlib.OAuth2Session` Returns: Tuple[requests_oauthlib.OAuth2Session, Mapping[str, Any]]: The new oauthlib session and the validated client configuration. .. _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) return session_from_client_config(client_config, scopes, **kwargs) def credentials_from_session(session, client_config=None): """Creates :class:`google.oauth2.credentials.Credentials` from a :class:`requests_oauthlib.OAuth2Session`. :meth:`fetch_token` must be called on the session before before calling this. This uses the session's auth token and the provided client configuration to create :class:`google.oauth2.credentials.Credentials`. This allows you to use the credentials from the session with Google API client libraries. Args: session (requests_oauthlib.OAuth2Session): The OAuth 2.0 session. client_config (Mapping[str, Any]): The subset of the client configuration to use. For example, if you have a web client you would pass in `client_config['web']`. Returns: google.oauth2.credentials.Credentials: The constructed credentials. Raises: ValueError: If there is no access token in the session. """ client_config = client_config if client_config is not None else {} if not session.token: raise ValueError( "There is no access token for this session, did you call " "fetch_token?" ) 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/interactive.py0000664000000000017530000001365714536115127022502 0ustar00root# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # 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 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Get user credentials from interactive code environments. This module contains helpers for getting user credentials from interactive code environments installed on a development machine, such as Jupyter notebooks. """ from __future__ import absolute_import import contextlib import socket import google_auth_oauthlib.flow 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 the OAuth 2.0 flow. You'll open a browser window to authenticate to your Google account. The permissions it requests correspond to the scopes you've provided. To obtain the ``client_id`` and ``client_secret``, create an **OAuth client ID** with application type **Other** from the `Credentials page on the Google Developer's Console `_. Learn more with the `Authenticating as an end user `_ guide. Args: scopes (Sequence[str]): A list of scopes to use when authenticating to Google APIs. See the `list of OAuth 2.0 scopes for Google APIs `_. client_id (str): A string that identifies your application to Google APIs. Find this value in the `Credentials page on the Google Developer's Console `_. client_secret (str): A string that verifies your application to Google APIs. Find this value in the `Credentials page on the Google Developer's Console `_. 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: The OAuth 2.0 credentials for the user. Examples: Get credentials for your user account and use them to run a query with BigQuery:: import google_auth_oauthlib # TODO: Create a client ID for your project. client_id = "YOUR-CLIENT-ID.apps.googleusercontent.com" client_secret = "abc_ThIsIsAsEcReT" # TODO: Choose the needed scopes for your applications. scopes = ["https://www.googleapis.com/auth/cloud-platform"] credentials = google_auth_oauthlib.get_user_credentials( scopes, client_id, client_secret ) # 1. Open the link. # 2. Authorize the application to have access to your account. # 3. Copy and paste the authorization code to the prompt. # Use the credentials to construct a client for Google APIs. from google.cloud import bigquery bigquery_client = bigquery.Client( credentials=credentials, project="your-project-id" ) print(list(bigquery_client.query("SELECT 1").result())) """ client_config = { "installed": { "client_id": client_id, "client_secret": client_secret, "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", } } app_flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_config( client_config, scopes=scopes ) 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/google_auth_oauthlib/tool/0002755000000000017530000000000014536115377020563 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/tool/__init__.py0000664000000000017530000000000014536115127022653 0ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib/tool/__main__.py0000664000000000017530000000737314536115127022660 0ustar00root# Copyright (C) 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Command-line tool for obtaining authorization and credentials from a user. This tool uses the OAuth 2.0 Authorization Code grant as described in `section 1.3.1 of RFC6749`_ and implemeted by :class:`google_auth_oauthlib.flow.Flow`. 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. This is not intended for production use where a combination of companion and on-device applications should complete the OAuth 2.0 authorization flow to get authorization from the users. .. _section 1.3.1 of RFC6749: https://tools.ietf.org/html/rfc6749#section-1.3.1 """ import json import os import os.path import click import google_auth_oauthlib.flow APP_NAME = "google-oauthlib-tool" DEFAULT_CREDENTIALS_FILENAME = "credentials.json" @click.command() @click.option( "--client-secrets", metavar="", required=True, help="Path to OAuth2 client secret JSON file.", ) @click.option( "--scope", multiple=True, metavar="", required=True, help="API scopes to authorize access for.", ) @click.option( "--save", is_flag=True, metavar="", show_default=True, default=False, help="Save the credentials to file.", ) @click.option( "--credentials", metavar="", show_default=True, default=os.path.join(click.get_app_dir(APP_NAME), DEFAULT_CREDENTIALS_FILENAME), help="Path to store OAuth2 credentials.", ) 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 in section 1.3.1 of RFC6749: https://tools.ietf.org/html/rfc6749#section-1.3.1 This tool is intended for assist developers in obtaining credentials 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 authorization flow to get authorization from the users. """ flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file( client_secrets, scopes=scope ) creds = flow.run_local_server() creds_data = { "token": creds.token, "refresh_token": creds.refresh_token, "token_uri": creds.token_uri, "client_id": creds.client_id, "client_secret": creds.client_secret, "scopes": creds.scopes, } if save: del creds_data["token"] config_path = os.path.dirname(credentials) if config_path and not os.path.isdir(config_path): os.makedirs(config_path) with open(credentials, "w") as outfile: json.dump(creds_data, outfile) click.echo("credentials saved: %s" % credentials) else: click.echo(json.dumps(creds_data)) if __name__ == "__main__": # pylint doesn't realize that click has changed the function signature. main() # pylint: disable=no-value-for-parameter ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/0002755000000000017530000000000014536115377021300 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/PKG-INFO0000644000000000017530000000520414536115377022374 0ustar00rootMetadata-Version: 2.1 Name: google-auth-oauthlib Version: 1.2.0 Summary: Google Authentication Library Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib Author: Google Cloud Platform Author-email: googleapis-packages@google.com License: Apache 2.0 Keywords: google auth oauth client oauthlib 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: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: OS Independent Classifier: Topic :: Internet :: WWW/HTTP Requires-Python: >=3.6 License-File: LICENSE Requires-Dist: google-auth>=2.15.0 Requires-Dist: requests-oauthlib>=0.7.0 Provides-Extra: tool Requires-Dist: click>=6.0.0; extra == "tool" 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/SOURCES.txt0000644000000000017530000000124414536115377023163 0ustar00rootLICENSE MANIFEST.in README.rst setup.cfg setup.py docs/conf.py google_auth_oauthlib/__init__.py google_auth_oauthlib/flow.py google_auth_oauthlib/helpers.py google_auth_oauthlib/interactive.py google_auth_oauthlib.egg-info/PKG-INFO google_auth_oauthlib.egg-info/SOURCES.txt google_auth_oauthlib.egg-info/dependency_links.txt google_auth_oauthlib.egg-info/entry_points.txt google_auth_oauthlib.egg-info/requires.txt google_auth_oauthlib.egg-info/top_level.txt google_auth_oauthlib/tool/__init__.py google_auth_oauthlib/tool/__main__.py tests/unit/test_flow.py tests/unit/test_helpers.py tests/unit/test_interactive.py tests/unit/test_tool.py tests/unit/data/client_secrets.json././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/dependency_links.txt0000644000000000017530000000000114536115377025344 0ustar00root ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/entry_points.txt0000644000000000017530000000013014536115377024566 0ustar00root[console_scripts] google-oauthlib-tool = google_auth_oauthlib.tool.__main__:main [tool] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/requires.txt0000644000000000017530000000010214536115377023667 0ustar00rootgoogle-auth>=2.15.0 requests-oauthlib>=0.7.0 [tool] click>=6.0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402815.0 google-auth-oauthlib-1.2.0/google_auth_oauthlib.egg-info/top_level.txt0000644000000000017530000000005214536115377024025 0ustar00rootdocs google_auth_oauthlib scripts testing ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/setup.cfg0000664000000000017530000000010314536115377015235 0ustar00root[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/setup.py0000664000000000017530000000452314536115127015131 0ustar00root# Copyright 2014 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import io from setuptools import find_namespace_packages from setuptools import setup TOOL_DEPENDENCIES = "click>=6.0.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 = "1.2.0" setup( name="google-auth-oauthlib", version=version, author="Google Cloud Platform", author_email="googleapis-packages@google.com", description="Google Authentication Library", long_description=long_description, url="https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib", packages=find_namespace_packages(exclude=("tests*",)), install_requires=DEPENDENCIES, extras_require={"tool": TOOL_DEPENDENCIES}, entry_points={ "console_scripts": [ "google-oauthlib-tool" "=google_auth_oauthlib.tool.__main__:main [tool]" ] }, python_requires=">=3.6", license="Apache 2.0", keywords="google auth oauth client oauthlib", classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Operating System :: OS Independent", "Topic :: Internet :: WWW/HTTP", ], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3222244 google-auth-oauthlib-1.2.0/tests/0002755000000000017530000000000014536115377014564 5ustar00root././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/tests/unit/0002755000000000017530000000000014536115377015543 5ustar00root././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1702402815.3262248 google-auth-oauthlib-1.2.0/tests/unit/data/0002755000000000017530000000000014536115377016454 5ustar00root././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/tests/unit/data/client_secrets.json0000664000000000017530000000062714536115127022353 0ustar00root{ "web": { "client_id": "example.apps.googleusercontent.com", "project_id": "example", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "itsasecrettoeveryone", "redirect_uris": [ "http://localhost" ] } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/tests/unit/test_flow.py0000664000000000017530000004111214536115127020113 0ustar00root# Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import concurrent.futures import datetime from functools import partial import json import os import re import random import socket import mock import pytest import requests import urllib from google_auth_oauthlib import flow DATA_DIR = os.path.join(os.path.dirname(__file__), "data") CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") with open(CLIENT_SECRETS_FILE, "r") as fh: CLIENT_SECRETS_INFO = json.load(fh) class TestFlow(object): def test_from_client_secrets_file(self): instance = flow.Flow.from_client_secrets_file( CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes ) assert instance.client_config == CLIENT_SECRETS_INFO["web"] assert ( instance.oauth2session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] ) assert instance.oauth2session.scope == mock.sentinel.scopes def test_from_client_secrets_file_with_redirect_uri(self): instance = flow.Flow.from_client_secrets_file( CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes, redirect_uri=mock.sentinel.redirect_uri, ) assert ( instance.redirect_uri == instance.oauth2session.redirect_uri == mock.sentinel.redirect_uri ) def test_from_client_config_installed(self): client_config = {"installed": CLIENT_SECRETS_INFO["web"]} instance = flow.Flow.from_client_config( client_config, scopes=mock.sentinel.scopes ) assert instance.client_config == client_config["installed"] assert ( instance.oauth2session.client_id == client_config["installed"]["client_id"] ) assert instance.oauth2session.scope == mock.sentinel.scopes def test_from_client_config_with_redirect_uri(self): client_config = {"installed": CLIENT_SECRETS_INFO["web"]} instance = flow.Flow.from_client_config( client_config, scopes=mock.sentinel.scopes, redirect_uri=mock.sentinel.redirect_uri, ) assert ( instance.redirect_uri == instance.oauth2session.redirect_uri == mock.sentinel.redirect_uri ) def test_from_client_config_bad_format(self): with pytest.raises(ValueError): flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes) @pytest.fixture def instance(self): yield flow.Flow.from_client_config( CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes ) def test_redirect_uri(self, instance): instance.redirect_uri = mock.sentinel.redirect_uri assert ( instance.redirect_uri == instance.oauth2session.redirect_uri == mock.sentinel.redirect_uri ) def test_authorization_url(self, instance): scope = "scope_one" instance.oauth2session.scope = [scope] authorization_url_patch = mock.patch.object( instance.oauth2session, "authorization_url", wraps=instance.oauth2session.authorization_url, ) with authorization_url_patch as authorization_url_spy: url, _ = instance.authorization_url(prompt="consent") assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url assert scope in url authorization_url_spy.assert_called_with( CLIENT_SECRETS_INFO["web"]["auth_uri"], access_type="offline", prompt="consent", ) def test_authorization_url_code_verifier(self, instance): scope = "scope_one" instance.oauth2session.scope = [scope] instance.code_verifier = "amanaplanacanalpanama" authorization_url_patch = mock.patch.object( instance.oauth2session, "authorization_url", wraps=instance.oauth2session.authorization_url, ) with authorization_url_patch as authorization_url_spy: url, _ = instance.authorization_url(prompt="consent") assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url assert scope in url authorization_url_spy.assert_called_with( CLIENT_SECRETS_INFO["web"]["auth_uri"], access_type="offline", prompt="consent", code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q", code_challenge_method="S256", ) def test_authorization_url_access_type(self, instance): scope = "scope_one" instance.oauth2session.scope = [scope] instance.code_verifier = "amanaplanacanalpanama" authorization_url_patch = mock.patch.object( instance.oauth2session, "authorization_url", wraps=instance.oauth2session.authorization_url, ) with authorization_url_patch as authorization_url_spy: url, _ = instance.authorization_url(access_type="meep") assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url assert scope in url authorization_url_spy.assert_called_with( CLIENT_SECRETS_INFO["web"]["auth_uri"], access_type="meep", code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q", code_challenge_method="S256", ) def test_authorization_url_generated_verifier(self): scope = "scope_one" instance = flow.Flow.from_client_config( CLIENT_SECRETS_INFO, scopes=[scope], autogenerate_code_verifier=True ) authorization_url_path = mock.patch.object( instance.oauth2session, "authorization_url", wraps=instance.oauth2session.authorization_url, ) with authorization_url_path as authorization_url_spy: instance.authorization_url() _, kwargs = authorization_url_spy.call_args_list[0] assert kwargs["code_challenge_method"] == "S256" assert len(instance.code_verifier) == 128 assert len(kwargs["code_challenge"]) == 43 valid_verifier = r"^[A-Za-z0-9-._~]*$" valid_challenge = r"^[A-Za-z0-9-_]*$" assert re.match(valid_verifier, instance.code_verifier) assert re.match(valid_challenge, kwargs["code_challenge"]) def test_fetch_token(self, instance): instance.code_verifier = "amanaplanacanalpanama" fetch_token_patch = mock.patch.object( instance.oauth2session, "fetch_token", autospec=True, return_value=mock.sentinel.token, ) with fetch_token_patch as fetch_token_mock: token = instance.fetch_token(code=mock.sentinel.code) assert token == mock.sentinel.token fetch_token_mock.assert_called_with( CLIENT_SECRETS_INFO["web"]["token_uri"], client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], code=mock.sentinel.code, code_verifier="amanaplanacanalpanama", ) def test_credentials(self, instance): instance.oauth2session.token = { "access_token": mock.sentinel.access_token, "refresh_token": mock.sentinel.refresh_token, "id_token": mock.sentinel.id_token, "expires_at": 643969200.0, } credentials = instance.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"] def test_authorized_session(self, instance): instance.oauth2session.token = { "access_token": mock.sentinel.access_token, "refresh_token": mock.sentinel.refresh_token, "id_token": mock.sentinel.id_token, "expires_at": 643969200.0, } session = instance.authorized_session() assert session.credentials.token == mock.sentinel.access_token class TestInstalledAppFlow(object): SCOPES = ["email", "profile"] REDIRECT_REQUEST_PATH = "/?code=code&state=state" AUDIENCE = "dummy-audience" @pytest.fixture def instance(self): yield flow.InstalledAppFlow.from_client_config( CLIENT_SECRETS_INFO, scopes=self.SCOPES ) @pytest.fixture def port(self): # Creating a new server at the same port will result in # a 'Address already in use' error for a brief # period of time after the socket has been closed. # Work around this in the tests by choosing a random port. # https://stackoverflow.com/questions/6380057/python-binding-socket-address-already-in-use yield random.randrange(60400, 60900) @pytest.fixture def socket(self, port): s = socket.socket() s.bind(("localhost", port)) yield s s.close() @pytest.fixture def mock_fetch_token(self, instance): def set_token(*args, **kwargs): instance.oauth2session.token = { "access_token": mock.sentinel.access_token, "refresh_token": mock.sentinel.refresh_token, "id_token": mock.sentinel.id_token, "expires_at": 643969200.0, } fetch_token_patch = mock.patch.object( instance.oauth2session, "fetch_token", autospec=True, side_effect=set_token ) with fetch_token_patch as fetch_token_mock: yield fetch_token_mock @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): auth_redirect_url = urllib.parse.urljoin( f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH ) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(partial(instance.run_local_server, port=port)) while not future.done(): try: requests.get(auth_redirect_url) except requests.ConnectionError: # pragma: NO COVER pass credentials = future.result() assert credentials.token == mock.sentinel.access_token assert credentials._refresh_token == mock.sentinel.refresh_token assert credentials.id_token == mock.sentinel.id_token assert webbrowser_mock.get().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( CLIENT_SECRETS_INFO["web"]["token_uri"], client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], authorization_response=expected_auth_response, code_verifier=None, audience=None, ) @pytest.mark.webtest @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) def test_run_local_server_audience( self, webbrowser_mock, instance, mock_fetch_token, port ): auth_redirect_url = urllib.parse.urljoin( f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH ) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit( partial( instance.run_local_server, port=port, token_audience=self.AUDIENCE ) ) while not future.done(): try: requests.get(auth_redirect_url) except requests.ConnectionError: # pragma: NO COVER pass credentials = future.result() assert credentials.token == mock.sentinel.access_token assert credentials._refresh_token == mock.sentinel.refresh_token assert credentials.id_token == mock.sentinel.id_token assert webbrowser_mock.get().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( CLIENT_SECRETS_INFO["web"]["token_uri"], client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], authorization_response=expected_auth_response, code_verifier=None, audience=self.AUDIENCE, ) @pytest.mark.webtest @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) def test_run_local_server_code_verifier( self, webbrowser_mock, instance, mock_fetch_token, port ): auth_redirect_url = urllib.parse.urljoin( f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH ) instance.code_verifier = "amanaplanacanalpanama" with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit( partial( instance.run_local_server, port=port, redirect_uri_trailing_slash=False, ) ) while not future.done(): try: requests.get(auth_redirect_url) except requests.ConnectionError: # pragma: NO COVER pass credentials = future.result() assert credentials.token == mock.sentinel.access_token assert credentials._refresh_token == mock.sentinel.refresh_token assert credentials.id_token == mock.sentinel.id_token assert webbrowser_mock.get().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( CLIENT_SECRETS_INFO["web"]["token_uri"], client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"], authorization_response=expected_auth_response, code_verifier="amanaplanacanalpanama", audience=None, ) @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True) @mock.patch("wsgiref.simple_server.make_server", autospec=True) def test_run_local_server_no_browser( 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 instance.run_local_server(open_browser=False) assert not webbrowser_mock.get().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.get().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( self, webbrowser_mock, instance, mock_fetch_token, port, socket ): # socket fixture is already bound to http://localhost:port instance.run_local_server with pytest.raises(OSError) as exc: instance.run_local_server(port=port) assert "address already in use" in exc.strerror.lower() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/tests/unit/test_helpers.py0000664000000000017530000001347714536115127020623 0ustar00root# Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import json 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") CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") with open(CLIENT_SECRETS_FILE, "r") as fh: CLIENT_SECRETS_INFO = json.load(fh) def test_session_from_client_config_web(): session, config = helpers.session_from_client_config( CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes ) assert config == CLIENT_SECRETS_INFO assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] assert session.scope == mock.sentinel.scopes def test_session_from_client_config_installed(): info = {"installed": CLIENT_SECRETS_INFO["web"]} session, config = helpers.session_from_client_config( info, scopes=mock.sentinel.scopes ) assert config == info assert session.client_id == info["installed"]["client_id"] assert session.scope == mock.sentinel.scopes def test_session_from_client_config_bad_format(): with pytest.raises(ValueError): helpers.session_from_client_config({}, scopes=[]) def test_session_from_client_config_missing_keys(): with pytest.raises(ValueError): helpers.session_from_client_config({"web": {}}, scopes=[]) def test_session_from_client_secrets_file(): session, config = helpers.session_from_client_secrets_file( CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes ) assert config == CLIENT_SECRETS_INFO assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"] assert session.scope == mock.sentinel.scopes @pytest.fixture def session(): session, _ = helpers.session_from_client_config( CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes ) yield session def test_credentials_from_session(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, } 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 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): with pytest.raises(ValueError): helpers.credentials_from_session(session) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/tests/unit/test_interactive.py0000664000000000017530000000700314536115127021462 0ustar00root# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # 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 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 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(): from google_auth_oauthlib import flow from google_auth_oauthlib import interactive as module_under_test mock_flow_instance = mock.create_autospec(flow.InstalledAppFlow, instance=True) with mock.patch( "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True ) as mock_flow: mock_flow.from_client_config.return_value = mock_flow_instance module_under_test.get_user_credentials( ["scopes"], "some-client-id", "shh-secret" ) mock_flow.from_client_config.assert_called_once_with(mock.ANY, scopes=["scopes"]) 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_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() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702402647.0 google-auth-oauthlib-1.2.0/tests/unit/test_tool.py0000664000000000017530000001141714536115127020126 0ustar00root# Copyright 2017 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # 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, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import io import json import os.path import tempfile import click.testing import google.oauth2.credentials import mock import pytest import google_auth_oauthlib.flow import google_auth_oauthlib.tool.__main__ as cli DATA_DIR = os.path.join(os.path.dirname(__file__), "data") CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") class TestMain(object): @pytest.fixture def runner(self): return click.testing.CliRunner() @pytest.fixture def dummy_credentials(self): return google.oauth2.credentials.Credentials( token="dummy_access_token", refresh_token="dummy_refresh_token", token_uri="dummy_token_uri", client_id="dummy_client_id", client_secret="dummy_client_secret", scopes=["dummy_scope1", "dummy_scope2"], ) @pytest.fixture def local_server_mock(self, dummy_credentials): run_local_server_patch = mock.patch.object( google_auth_oauthlib.flow.InstalledAppFlow, "run_local_server", autospec=True, ) with run_local_server_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 assert "RFC6749" in result.output assert "OAuth 2.0 authorization flow" in result.output assert "not intended for production use" in result.output assert result.exit_code == 0 def test_defaults(self, runner, dummy_credentials, local_server_mock): result = runner.invoke( cli.main, ["--client-secrets", CLIENT_SECRETS_FILE, "--scope", "somescope"] ) local_server_mock.assert_called_with(mock.ANY) assert not result.exception assert result.exit_code == 0 creds_data = json.loads(result.output) creds = google.oauth2.credentials.Credentials(**creds_data) assert creds.token == dummy_credentials.token assert creds.refresh_token == dummy_credentials.refresh_token assert creds.token_uri == dummy_credentials.token_uri assert creds.client_id == dummy_credentials.client_id assert creds.client_secret == dummy_credentials.client_secret assert creds.scopes == dummy_credentials.scopes def test_save_new_dir(self, runner, dummy_credentials, local_server_mock): credentials_tmpdir = tempfile.mkdtemp() credentials_path = os.path.join( credentials_tmpdir, "new-directory", "credentials.json" ) result = runner.invoke( cli.main, [ "--client-secrets", CLIENT_SECRETS_FILE, "--scope", "somescope", "--credentials", credentials_path, "--save", ], ) local_server_mock.assert_called_with(mock.ANY) assert not result.exception assert "saved" in result.output assert result.exit_code == 0 with io.open(credentials_path) as f: # pylint: disable=invalid-name creds_data = json.load(f) assert "access_token" not in creds_data creds = google.oauth2.credentials.Credentials(token=None, **creds_data) assert creds.token is None assert creds.refresh_token == dummy_credentials.refresh_token assert creds.token_uri == dummy_credentials.token_uri assert creds.client_id == dummy_credentials.client_id assert creds.client_secret == dummy_credentials.client_secret def test_save_existing_dir(self, runner, local_server_mock): credentials_tmpdir = tempfile.mkdtemp() result = runner.invoke( cli.main, [ "--client-secrets", CLIENT_SECRETS_FILE, "--scope", "somescope", "--credentials", os.path.join(credentials_tmpdir, "credentials.json"), "--save", ], ) local_server_mock.assert_called_with(mock.ANY) assert not result.exception assert "saved" in result.output assert result.exit_code == 0