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

Issue:
Feature Request: Ability to specify an OAuth audience to InstalledAppFlow.run_local_server
 **Is your feature request related to a problem? Please describe.**

I'm using Google's Identity Aware Proxy to provide an OAuth layer. When programmatically accessing an IAP protected resource, one is expected to provide a specific audience to the call to get an access token ([source](https://cloud.google.com/iap/docs/authentication-howto#signing_in_to_the_application)).  AFAICT This is not currently possible with `InstalledAppFlow.run_local_server`

 **Describe the solution you'd like**

A new argument to `run_local_server` that passes the audience through to the `fetch_token` call

 **Describe alternatives you've considered**

not aware of any alternatives

 **Additional context**

I've got this setup working internally with a live IAP application, having copied the body of `run_local_server` and modified it as above.



Discussion:
google-cla[bot]: Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this [failed invocation](https://github.com/googleapis/google-auth-library-python-oauthlib/pull/300/checks?check_run_id=15542506123) of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.
keriwarr: oh turns out my attempt to fill out the CLA failed because my organization has already signed one, I'll get myself added to the list
keriwarr: ok I think I've joined the CLA google group for my org
keriwarr: @clundin25 what's the next step? can we get this approved?
keriwarr: @clundin25 what's up with this "OwlBot Post Processor" status check?
clundin25: @keriwarr It just needs to be manually triggered. It should pass with no problem
dandhlee: OwlBot is our friendly bot for keeping things in sync with the rest of our googleapis/ repos. It usually doesn't run for external contributors and forks, I've kicked off the job manually :)
clundin25: @keriwarr Can you confirm this code was needed so that the identity token had the correct audience?
clundin25: I will merge after @sai-sunder-s has taken a look as well.
keriwarr: > @keriwarr Can you confirm this code was needed so that the identity token had the correct audience?

replied here: https://github.com/googleapis/google-auth-library-python-oauthlib/issues/299

Patch:
diff --git a/google_auth_oauthlib/flow.py b/google_auth_oauthlib/flow.py
index 7d8e521..f55a023 100644
--- a/google_auth_oauthlib/flow.py
+++ b/google_auth_oauthlib/flow.py
@@ -378,6 +378,7 @@ class InstalledAppFlow(Flow):
         open_browser=True,
         redirect_uri_trailing_slash=True,
         timeout_seconds=None,
+        token_audience=None,
         **kwargs
     ):
         """Run the flow using the server strategy.
@@ -412,6 +413,9 @@ class InstalledAppFlow(Flow):
                 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.
             kwargs: Additional keyword arguments passed through to
                 :meth:`authorization_url`.
 
@@ -444,7 +448,9 @@ class InstalledAppFlow(Flow):
         # 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)
+        self.fetch_token(
+            authorization_response=authorization_response, audience=token_audience
+        )
 
         # This closes the socket
         local_server.server_close()


Test patch:
diff --git a/tests/unit/test_flow.py b/tests/unit/test_flow.py
index d9c9f8b..103bffd 100644
--- a/tests/unit/test_flow.py
+++ b/tests/unit/test_flow.py
@@ -242,6 +242,7 @@ class TestFlow(object):
 class TestInstalledAppFlow(object):
     SCOPES = ["email", "profile"]
     REDIRECT_REQUEST_PATH = "/?code=code&state=state"
+    AUDIENCE = "dummy-audience"
 
     @pytest.fixture
     def instance(self):
@@ -312,6 +313,46 @@ class TestInstalledAppFlow(object):
             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.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
@@ -353,6 +394,7 @@ class TestInstalledAppFlow(object):
             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)