Skip to content

Commit 1fb3c8e

Browse files
feat: enable self signed jwt for service account credentials (#1553)
Enable self signed jwt if google-auth service account credentials are used. Tested with Storage, Compute and PubSub APIs: ``` import googleapiclient.discovery project = "<project>" zone = "us-west1-a" compute = googleapiclient.discovery.build('compute', 'v1') result = compute.instances().list(project=project, zone=zone).execute() print(result) storage = googleapiclient.discovery.build('storage', 'v1') result = storage.buckets().list(project=project).execute() print(result) topic = "<topic>" pubsub = googleapiclient.discovery.build('pubsub', 'v1') result = pubsub.projects().topics().get(topic=f"projects/{project}/topics/{topic}").execute() print(result) ```
1 parent fc365b8 commit 1fb3c8e

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

googleapiclient/discovery.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import google.api_core.client_options
4545
from google.auth.transport import mtls
4646
from google.auth.exceptions import MutualTLSChannelError
47+
from google.oauth2 import service_account
4748

4849
try:
4950
import google_auth_httplib2
@@ -188,6 +189,7 @@ def build(
188189
adc_key_path=None,
189190
num_retries=1,
190191
static_discovery=None,
192+
always_use_jwt_access=True,
191193
):
192194
"""Construct a Resource for interacting with an API.
193195
@@ -246,6 +248,9 @@ def build(
246248
on the value of `discoveryServiceUrl`. `static_discovery` will default to
247249
`True` when `discoveryServiceUrl` is also not provided, otherwise it will
248250
default to `False`.
251+
always_use_jwt_access: Boolean, whether always use self signed JWT for service
252+
account credentials. This only applies to
253+
google.oauth2.service_account.Credentials.
249254
250255
Returns:
251256
A Resource object with methods for interacting with the service.
@@ -301,6 +306,7 @@ def build(
301306
client_options=client_options,
302307
adc_cert_path=adc_cert_path,
303308
adc_key_path=adc_key_path,
309+
always_use_jwt_access=always_use_jwt_access,
304310
)
305311
break # exit if a service was created
306312
except HttpError as e:
@@ -441,6 +447,7 @@ def build_from_document(
441447
client_options=None,
442448
adc_cert_path=None,
443449
adc_key_path=None,
450+
always_use_jwt_access=True,
444451
):
445452
"""Create a Resource for interacting with an API.
446453
@@ -490,6 +497,9 @@ def build_from_document(
490497
`true` in order to use this field, otherwise this field doesn't nothing.
491498
More details on the environment variables are here:
492499
https://google.aip.dev/auth/4114
500+
always_use_jwt_access: Boolean, whether always use self signed JWT for service
501+
account credentials. This only applies to
502+
google.oauth2.service_account.Credentials.
493503
494504
Returns:
495505
A Resource object with methods for interacting with the service.
@@ -530,6 +540,7 @@ def build_from_document(
530540

531541
# If an API Endpoint is provided on client options, use that as the base URL
532542
base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"])
543+
audience_for_self_signed_jwt = base
533544
if client_options.api_endpoint:
534545
base = client_options.api_endpoint
535546

@@ -572,6 +583,17 @@ def build_from_document(
572583
if not client_options.scopes:
573584
credentials = _auth.with_scopes(credentials, scopes)
574585

586+
# For google-auth service account credentials, enable self signed JWT if
587+
# always_use_jwt_access is true.
588+
if (
589+
credentials
590+
and isinstance(credentials, service_account.Credentials)
591+
and always_use_jwt_access
592+
and hasattr(service_account.Credentials, "with_always_use_jwt_access")
593+
):
594+
credentials = credentials.with_always_use_jwt_access(always_use_jwt_access)
595+
credentials._create_self_signed_jwt(audience_for_self_signed_jwt)
596+
575597
# If credentials are provided, create an authorized http instance;
576598
# otherwise, skip authentication.
577599
if credentials:

tests/data/service_account.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "example-project",
4+
"private_key_id": "1",
5+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
6+
"client_email": "[email protected]",
7+
"client_id": "1234",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://accounts.google.com/o/oauth2/token"
10+
}

tests/test_discovery.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,33 @@ def test_credentials_file_from_client_options(self):
685685
"credentials.json", scopes=None, quota_project_id=None
686686
)
687687

688+
def test_self_signed_jwt_enabled(self):
689+
service_account_file_path = os.path.join(DATA_DIR, "service_account.json")
690+
creds = google.oauth2.service_account.Credentials.from_service_account_file(service_account_file_path)
691+
692+
discovery = read_datafile("logging.json")
693+
694+
with mock.patch("google.oauth2.service_account.Credentials._create_self_signed_jwt") as _create_self_signed_jwt:
695+
build_from_document(
696+
discovery,
697+
credentials=creds,
698+
)
699+
_create_self_signed_jwt.assert_called_with("https://logging.googleapis.com/")
700+
701+
def test_self_signed_jwt_disabled(self):
702+
service_account_file_path = os.path.join(DATA_DIR, "service_account.json")
703+
creds = google.oauth2.service_account.Credentials.from_service_account_file(service_account_file_path)
704+
705+
discovery = read_datafile("logging.json")
706+
707+
with mock.patch("google.oauth2.service_account.Credentials._create_self_signed_jwt") as _create_self_signed_jwt:
708+
build_from_document(
709+
discovery,
710+
credentials=creds,
711+
always_use_jwt_access=False,
712+
)
713+
_create_self_signed_jwt.assert_not_called()
714+
688715

689716
REGULAR_ENDPOINT = "https://www.googleapis.com/plus/v1/"
690717
MTLS_ENDPOINT = "https://www.mtls.googleapis.com/plus/v1/"

0 commit comments

Comments
 (0)