Repository: googleapis/google-auth-library-python-oauthlib

Issue:
Feature Request: `get_user_credentials` method that runs the installed application flow
There are times in development and the data scientist workflow, where it is useful to have explicit user-based credentials. I happen to like the way I did this in the `pydata-google-auth` library and would like to contribute it here, if you're open to it.

**pydata-google-auth**

```
pydata_google_auth.get_user_credentials(
    scopes,
    client_id=None,
    client_secret=None,
    credentials_cache=<pydata_google_auth.cache.ReadWriteCredentialsCache object>,
    use_local_webserver=False)
```

**google-auth-oauthlib**

```
google_auth_oauthlib.get_user_credentials(
    scopes, client_id=None, client_secret=None, use_local_webserver=False)
```

Differences:

* I propose is that this library doesn't need to handle any cache mechanism. As I understand it, caching credentials is out-of-scope for this library.
* `client_id` and `client_secret` are still optional. Ideally, this would not require the developer / data scientist to download a client secret, as the ADC client secret could be used.


Discussion:
tswast: Some screenshots of this in action:

<img width="798" alt="Screen Shot 2019-04-26 at 2 49 25 PM" src="https://user-images.githubusercontent.com/247555/56841561-69e19600-6843-11e9-80ef-055840caae1d.png">

<img width="981" alt="Screen Shot 2019-04-26 at 2 53 43 PM" src="https://user-images.githubusercontent.com/247555/56841563-6b12c300-6843-11e9-99a4-f185a2d615db.png">

busunkim96: LGTM, but I think it would be best to have a few more people set eyes on it. 
busunkim96: I see more interest in this in https://github.com/googleapis/google-auth-library-python/issues/271, have you heard from anyone on this?
theacodes: I *really* don't think we should perpetuate the use of that shared client
ID. I would chat with Hong before going forward here.

On Tue, Jun 4, 2019 at 9:34 AM Bu Sun Kim <notifications@github.com> wrote:

> I see more interest in this in googleapis/google-auth-library-python#271
> <https://github.com/googleapis/google-auth-library-python/issues/271>,
> have you heard from anyone on this?
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/googleapis/google-auth-library-python-oauthlib/pull/40?email_source=notifications&email_token=AAB5I44Q2OTUWWPZTYUFBDTPY2KRZA5CNFSM4HI24HF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODW5ETCA#issuecomment-498747784>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAB5I4Y2P3OIPFJ5HOZKZ6DPY2KRZANCNFSM4HI24HFQ>
> .
>

tswast: What if we make client ID and client secret required?
theacodes: That's fine.

On Tue, Jun 4, 2019 at 9:52 AM Tim Swast <notifications@github.com> wrote:

> What if we make client ID and client secret required?
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/googleapis/google-auth-library-python-oauthlib/pull/40?email_source=notifications&email_token=AAB5I43DU7HW6TWR5IQAW4LPY2MU5A5CNFSM4HI24HF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODW5GHGY#issuecomment-498754459>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAB5I4253QCXHRFPT2YMVK3PY2MU5ANCNFSM4HI24HFQ>
> .
>

tswast: @busunkim96 This PR is ready for another review.

Once this is merged, is autorelease configured for this repo?
busunkim96: It'll be a manual release. (looks like this repo still needs to be moved off of Travis, in fact :frowning:) 

Would you mind adding an example of how to use this in the comments like we have for `flow.py`?

[example for flow](https://github.com/googleapis/google-auth-library-python-oauthlib/blob/e56853e0b29e4b67943e2dd7cc361dea5eb94d2e/google_auth_oauthlib/flow.py#L23-L45)

tswast: > Would you mind adding an example of how to use this in the comments like we have for flow.py?

Great idea. Done! I've confirmed that it renders well in the docs.

Patch:
diff --git a/.gitignore b/.gitignore
index 1f65cf3..5bd8229 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,6 @@ tests/data/user-key.json
 # Generated files
 pylintrc
 pylintrc.test
+
+# Virtual environment
+env/
\ No newline at end of file
diff --git a/google_auth_oauthlib/__init__.py b/google_auth_oauthlib/__init__.py
index e69de29..1905f9a 100644
--- a/google_auth_oauthlib/__init__.py
+++ b/google_auth_oauthlib/__init__.py
@@ -0,0 +1,23 @@
+# 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 <https://oauthlib.readthedocs.io/>`__
+integration with `google-auth <https://google-auth.readthedocs.io/>`__.
+"""
+
+from .interactive import get_user_credentials
+
+__all__ = ["get_user_credentials"]
diff --git a/google_auth_oauthlib/interactive.py b/google_auth_oauthlib/interactive.py
new file mode 100644
index 0000000..c6a5d28
--- /dev/null
+++ b/google_auth_oauthlib/interactive.py
@@ -0,0 +1,105 @@
+# 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 google_auth_oauthlib.flow
+
+
+def get_user_credentials(scopes, client_id, client_secret):
+    """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
+    <https://console.developers.google.com/apis/credentials>`_. Learn more
+    with the `Authenticating as an end user
+    <https://cloud.google.com/docs/authentication/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
+            <https://developers.google.com/identity/protocols/googlescopes>`_.
+        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
+            <https://console.developers.google.com/apis/credentials>`_.
+        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
+            <https://console.developers.google.com/apis/credentials>`_.
+
+    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,
+            "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob"],
+            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+            "token_uri": "https://oauth2.googleapis.com/token",
+        }
+    }
+
+    app_flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_config(
+        client_config, scopes=scopes
+    )
+
+    return app_flow.run_console()
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..a6c1654
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,73 @@
+import nox
+
+
+@nox.session(python="3.6")
+def cover(session):
+    session.install("mock", "pytest", "pytest-cov", "futures", "click")
+    session.install(".")
+    session.run(
+        "py.test", "--cov=google_auth_oauthlib", "--cov=tests", "--cov-report=", "tests"
+    )
+    session.run("coverage", "report", "--show-missing", "--fail-under=100")
+
+
+@nox.session(python="3.6")
+def docgen(session):
+    session.env["SPHINX_APIDOC_OPTIONS"] = "members,inherited-members,show-inheritance"
+    session.install("mock", "pytest", "pytest-cov", "futures", "click", "sphinx")
+    session.install(".")
+    session.run("rm", "-rf", "docs/reference")
+    session.run(
+        "sphinx-apidoc",
+        "--output-dir",
+        "docs/reference",
+        "--separate",
+        "--module-first",
+        "google_auth_oauthlib",
+    )
+
+
+@nox.session(python="3.6")
+def docs(session):
+    session.install("sphinx", "-r", "docs/requirements-docs.txt")
+    session.install(".")
+    session.run("make", "-C", "docs", "html")
+
+
+@nox.session(python="3.5")
+def lint(session):
+    session.install(
+        "flake8",
+        "flake8-import-order",
+        "pylint",
+        "docutils",
+        "gcp-devrel-py-tools>=0.0.3",
+    )
+    session.install(".")
+    session.run(
+        "python", "setup.py", "check", "--metadata", "--restructuredtext", "--strict"
+    )
+    session.run(
+        "flake8",
+        "--import-order-style=google",
+        "--application-import-names=google_auth_oauthlib,tests",
+        "google_auth_oauthlib",
+        "tests",
+    )
+    session.run(
+        "gcp-devrel-py-tools",
+        "run-pylint",
+        "--config",
+        "pylint.config.py",
+        "--library-filesets",
+        "google_auth_oauthlib",
+        "--test-filesets",
+        "tests",
+    )
+
+
+@nox.session(python=["2.7", "3.5", "3.6", "3.7", "pypy"])
+def test(session):
+    session.install("mock", "pytest", "pytest-cov", "futures", "click")
+    session.install(".")
+    session.run("py.test", "--cov=google_auth_oauthlib", "--cov=tests", "tests")


Test patch:
diff --git a/tests/test_interactive.py b/tests/test_interactive.py
new file mode 100644
index 0000000..c5795bf
--- /dev/null
+++ b/tests/test_interactive.py
@@ -0,0 +1,44 @@
+# 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 mock
+
+
+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_console.assert_called_once()