Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit f154176

Browse files
committed
feat: support self-signed JWT flow for servie accounts
1 parent 35338fc commit f154176

13 files changed

Lines changed: 531 additions & 54 deletions

File tree

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
{% block content %}
44
import abc
55
import typing
6+
import packaging.version
67
import pkg_resources
78

89
from google import auth # type: ignore
10+
import google.api_core
911
from google.api_core import exceptions # type: ignore
1012
from google.api_core import gapic_v1 # type: ignore
1113
from google.api_core import retry as retries # type: ignore
@@ -34,6 +36,17 @@ try:
3436
except pkg_resources.DistributionNotFound:
3537
DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo()
3638

39+
try:
40+
# google.auth.__version__ was added in 1.26.0
41+
_GOOGLE_AUTH_VERSION = auth.__version__
42+
except AttributeError:
43+
try: # try pkg_resources if it is available
44+
_GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version
45+
except pkg_resources.DistributionNotFound: # pragma: NO COVER
46+
_GOOGLE_AUTH_VERSION = None
47+
48+
_API_CORE_VERSION = google.api_core.__version__
49+
3750
class {{ service.name }}Transport(abc.ABC):
3851
"""Abstract transport class for {{ service.name }}."""
3952

@@ -43,12 +56,14 @@ class {{ service.name }}Transport(abc.ABC):
4356
{%- endfor %}
4457
)
4558

59+
DEFAULT_HOST = {% if service.host %}'{{ service.host }}'{% else %}{{ None }}{% endif %}
60+
4661
def __init__(
4762
self, *,
48-
host: str{% if service.host %} = '{{ service.host }}'{% endif %},
63+
host: str = DEFAULT_HOST,
4964
credentials: credentials.Credentials = None,
5065
credentials_file: typing.Optional[str] = None,
51-
scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES,
66+
scopes: typing.Optional[typing.Sequence[str]] = None,
5267
quota_project_id: typing.Optional[str] = None,
5368
client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO,
5469
**kwargs,
@@ -66,7 +81,7 @@ class {{ service.name }}Transport(abc.ABC):
6681
credentials_file (Optional[str]): A file with credentials that can
6782
be loaded with :func:`google.auth.load_credentials_from_file`.
6883
This argument is mutually exclusive with credentials.
69-
scope (Optional[Sequence[str]]): A list of scopes.
84+
scopes (Optional[Sequence[str]]): A list of scopes.
7085
quota_project_id (Optional[str]): An optional project to use for billing
7186
and quota.
7287
client_info (google.api_core.gapic_v1.client_info.ClientInfo):
@@ -80,6 +95,21 @@ class {{ service.name }}Transport(abc.ABC):
8095
host += ':443'
8196
self._host = host
8297

98+
# If a custom API endpoint is set, set scopes to ensure the auth
99+
# library does not used the self-signed JWT flow for service
100+
# accounts
101+
if host != self.DEFAULT_HOST and not scopes:
102+
scopes = self.AUTH_SCOPES
103+
104+
# TODO: Remove this if/else once google-auth >= 1.25.0 is required
105+
if _GOOGLE_AUTH_VERSION and (
106+
packaging.version.parse(_GOOGLE_AUTH_VERSION)
107+
>= packaging.version.parse("1.25.0")
108+
):
109+
scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES}
110+
else:
111+
scopes_kwargs = {"scopes": scopes if scopes is not None else self.AUTH_SCOPES}
112+
83113
# If no credentials are provided, then determine the appropriate
84114
# defaults.
85115
if credentials and credentials_file:
@@ -88,12 +118,12 @@ class {{ service.name }}Transport(abc.ABC):
88118
if credentials_file is not None:
89119
credentials, _ = auth.load_credentials_from_file(
90120
credentials_file,
91-
scopes=scopes,
121+
**scopes_kwargs,
92122
quota_project_id=quota_project_id
93123
)
94124

95125
elif credentials is None:
96-
credentials, _ = auth.default(scopes=scopes, quota_project_id=quota_project_id)
126+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
97127

98128
# Save the credentials.
99129
self._credentials = credentials

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import warnings
55
from typing import Callable, Dict, Optional, Sequence, Tuple
66

7+
import google.api_core
78
from google.api_core import grpc_helpers # type: ignore
89
{%- if service.has_lro %}
910
from google.api_core import operations_v1 # type: ignore
@@ -12,6 +13,8 @@ from google.api_core import gapic_v1 # type: ignore
1213
from google import auth # type: ignore
1314
from google.auth import credentials # type: ignore
1415
from google.auth.transport.grpc import SslCredentials # type: ignore
16+
import packaging.version
17+
import pkg_resources
1518

1619
import grpc # type: ignore
1720

@@ -26,6 +29,18 @@ from google.iam.v1 import policy_pb2 as policy # type: ignore
2629
{% endif %}
2730
{% endfilter %}
2831
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
32+
from .base import _GOOGLE_AUTH_VERSION, _API_CORE_VERSION
33+
34+
try:
35+
# google.auth.__version__ was added in 1.26.0
36+
_GOOGLE_AUTH_VERSION = auth.__version__
37+
except AttributeError:
38+
try: # try pkg_resources if it is available
39+
_GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version
40+
except pkg_resources.DistributionNotFound: # pragma: NO COVER
41+
_GOOGLE_AUTH_VERSION = None
42+
43+
_API_CORE_VERSION = google.api_core.__version__
2944

3045

3146
class {{ service.name }}GrpcTransport({{ service.name }}Transport):
@@ -101,6 +116,22 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
101116
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
102117
and ``credentials_file`` are passed.
103118
"""
119+
120+
# If a custom API endpoint is set, set scopes to ensure the auth
121+
# library does not used the self-signed JWT flow for service
122+
# accounts
123+
if host != self.DEFAULT_HOST and not scopes:
124+
scopes = self.AUTH_SCOPES
125+
126+
# TODO(busunkim): Remove this if/else once google-auth >= 1.25.0 is required
127+
if _GOOGLE_AUTH_VERSION and (
128+
packaging.version.parse(_GOOGLE_AUTH_VERSION)
129+
>= packaging.version.parse("1.25.0")
130+
):
131+
scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES}
132+
else:
133+
scopes_kwargs = {"scopes": scopes or self.AUTH_SCOPES}
134+
104135
self._ssl_channel_credentials = ssl_channel_credentials
105136

106137
if api_mtls_endpoint:
@@ -120,7 +151,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
120151
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
121152

122153
if credentials is None:
123-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
154+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
124155

125156
# Create SSL credentials with client_cert_source or application
126157
# default SSL credentials.
@@ -138,7 +169,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
138169
credentials=credentials,
139170
credentials_file=credentials_file,
140171
ssl_credentials=ssl_credentials,
141-
scopes=scopes or self.AUTH_SCOPES,
172+
scopes=scopes,
142173
quota_project_id=quota_project_id,
143174
options=[
144175
("grpc.max_send_message_length", -1),
@@ -150,7 +181,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
150181
host = host if ":" in host else host + ":443"
151182

152183
if credentials is None:
153-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
184+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
154185

155186
if client_cert_source_for_mtls and not ssl_channel_credentials:
156187
cert, key = client_cert_source_for_mtls()
@@ -164,7 +195,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
164195
credentials=credentials,
165196
credentials_file=credentials_file,
166197
ssl_credentials=self._ssl_channel_credentials,
167-
scopes=scopes or self.AUTH_SCOPES,
198+
scopes=scopes,
168199
quota_project_id=quota_project_id,
169200
options=[
170201
("grpc.max_send_message_length", -1),
@@ -182,7 +213,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
182213
host=host,
183214
credentials=credentials,
184215
credentials_file=credentials_file,
185-
scopes=scopes or self.AUTH_SCOPES,
216+
scopes=scopes,
186217
quota_project_id=quota_project_id,
187218
client_info=client_info,
188219
)
@@ -220,13 +251,31 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
220251
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
221252
and ``credentials_file`` are passed.
222253
"""
223-
scopes = scopes or cls.AUTH_SCOPES
254+
# If a custom API endpoint is set, set scopes to ensure the auth
255+
# library does not used the self-signed JWT flow for service
256+
# accounts
257+
if host != cls.DEFAULT_HOST and not scopes:
258+
scopes = cls.AUTH_SCOPES
259+
260+
self_signed_jwt_kwargs = {}
261+
262+
# TODO(busunkim): Remove this if/else once google-api-core >= 1.26.0 is required
263+
if _API_CORE_VERSION and (
264+
packaging.version.parse(_API_CORE_VERSION)
265+
>= packaging.version.parse("1.26.0")
266+
):
267+
self_signed_jwt_kwargs["default_scopes"] = cls.AUTH_SCOPES
268+
self_signed_jwt_kwargs["scopes"] = scopes
269+
self_signed_jwt_kwargs["default_host"] = cls.DEFAULT_HOST
270+
else:
271+
self_signed_jwt_kwargs["scopes"] = scopes if scopes is not None else cls.AUTH_SCOPES
272+
224273
return grpc_helpers.create_channel(
225274
host,
226275
credentials=credentials,
227276
credentials_file=credentials_file,
228-
scopes=scopes,
229277
quota_project_id=quota_project_id,
278+
**self_signed_jwt_kwargs,
230279
**kwargs
231280
)
232281

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ from google.api_core import operations_v1 # type: ignore
1212
from google import auth # type: ignore
1313
from google.auth import credentials # type: ignore
1414
from google.auth.transport.grpc import SslCredentials # type: ignore
15+
import packaging.version
1516

1617
import grpc # type: ignore
1718
from grpc.experimental import aio # type: ignore
@@ -27,7 +28,10 @@ from google.iam.v1 import policy_pb2 as policy # type: ignore
2728
{% endif %}
2829
{% endfilter %}
2930
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
31+
from .base import _GOOGLE_AUTH_VERSION, _API_CORE_VERSION
3032
from .grpc import {{ service.name }}GrpcTransport
33+
from .grpc import _API_CORE_VERSION
34+
from .grpc import _GOOGLE_AUTH_VERSION
3135

3236

3337
class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
@@ -75,13 +79,31 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
7579
Returns:
7680
aio.Channel: A gRPC AsyncIO channel object.
7781
"""
78-
scopes = scopes or cls.AUTH_SCOPES
82+
# If a custom API endpoint is set, set scopes to ensure the auth
83+
# library does not used the self-signed JWT flow for service
84+
# accounts
85+
if host != cls.DEFAULT_HOST and not scopes:
86+
scopes = cls.AUTH_SCOPES
87+
88+
self_signed_jwt_kwargs = {}
89+
90+
# TODO(busunkim): Remove this if/else once google-api-core >= 1.26.0 is required
91+
if _API_CORE_VERSION and (
92+
packaging.version.parse(_API_CORE_VERSION)
93+
>= packaging.version.parse("1.26.0")
94+
):
95+
self_signed_jwt_kwargs["default_scopes"] = cls.AUTH_SCOPES
96+
self_signed_jwt_kwargs["scopes"] = scopes
97+
self_signed_jwt_kwargs["default_host"] = cls.DEFAULT_HOST
98+
else:
99+
self_signed_jwt_kwargs["scopes"] = scopes or cls.AUTH_SCOPES
100+
79101
return grpc_helpers_async.create_channel(
80102
host,
81103
credentials=credentials,
82104
credentials_file=credentials_file,
83-
scopes=scopes,
84105
quota_project_id=quota_project_id,
106+
**self_signed_jwt_kwargs,
85107
**kwargs
86108
)
87109

@@ -145,6 +167,21 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
145167
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
146168
and ``credentials_file`` are passed.
147169
"""
170+
# If a custom API endpoint is set, set scopes to ensure the auth
171+
# library does not used the self-signed JWT flow for service
172+
# accounts
173+
if host.split(":")[0] != self.DEFAULT_HOST and not scopes:
174+
scopes = self.AUTH_SCOPES
175+
176+
# TODO: Remove this if/else once google-auth >= 1.25.0 is required
177+
if _GOOGLE_AUTH_VERSION and packaging.version.parse(
178+
_GOOGLE_AUTH_VERSION
179+
) >= packaging.version.parse("1.25.0"):
180+
scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES}
181+
else:
182+
scopes_kwargs = {"scopes": scopes or self.AUTH_SCOPES}
183+
184+
148185
self._ssl_channel_credentials = ssl_channel_credentials
149186

150187
if api_mtls_endpoint:
@@ -164,7 +201,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
164201
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
165202

166203
if credentials is None:
167-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
204+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
168205

169206
# Create SSL credentials with client_cert_source or application
170207
# default SSL credentials.
@@ -182,7 +219,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
182219
credentials=credentials,
183220
credentials_file=credentials_file,
184221
ssl_credentials=ssl_credentials,
185-
scopes=scopes or self.AUTH_SCOPES,
222+
scopes=scopes,
186223
quota_project_id=quota_project_id,
187224
options=[
188225
("grpc.max_send_message_length", -1),
@@ -194,7 +231,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
194231
host = host if ":" in host else host + ":443"
195232

196233
if credentials is None:
197-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
234+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
198235

199236
if client_cert_source_for_mtls and not ssl_channel_credentials:
200237
cert, key = client_cert_source_for_mtls()
@@ -208,7 +245,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
208245
credentials=credentials,
209246
credentials_file=credentials_file,
210247
ssl_credentials=self._ssl_channel_credentials,
211-
scopes=scopes or self.AUTH_SCOPES,
248+
scopes=scopes,
212249
quota_project_id=quota_project_id,
213250
options=[
214251
("grpc.max_send_message_length", -1),
@@ -221,7 +258,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
221258
host=host,
222259
credentials=credentials,
223260
credentials_file=credentials_file,
224-
scopes=scopes or self.AUTH_SCOPES,
261+
scopes=scopes,
225262
quota_project_id=quota_project_id,
226263
client_info=client_info,
227264
)

gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,14 @@ class {{ service.name }}RestTransport({{ service.name }}Transport):
8181
"""
8282
# Run the base constructor
8383
# TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc.
84+
# When custom host (api_endpoint) can be set, `scopes` must *also* be set on the
85+
# credentials object
8486
super().__init__(
8587
host=host,
8688
credentials=credentials,
8789
client_info=client_info,
8890
)
89-
self._session = AuthorizedSession(self._credentials)
91+
self._session = AuthorizedSession(self._credentials, default_host=cls.DEFAULT_HOST)
9092
{%- if service.has_lro %}
9193
self._operations_client = None
9294
{%- endif %}
@@ -109,7 +111,9 @@ class {{ service.name }}RestTransport({{ service.name }}Transport):
109111
grpc_helpers.create_channel(
110112
self._host,
111113
credentials=self._credentials,
112-
scopes=self.AUTH_SCOPES,
114+
default_scopes=self.AUTH_SCOPES,
115+
scopes=scopes,
116+
default_host=cls.DEFAULT_HOST,
113117
options=[
114118
("grpc.max_send_message_length", -1),
115119
("grpc.max_receive_message_length", -1),

gapic/templates/.coveragerc.j2

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
branch = True
33

44
[report]
5-
fail_under = 100
65
show_missing = True
76
omit =
87
{{ api.naming.module_namespace|join("/") }}/{{ api.naming.module_name }}/__init__.py

0 commit comments

Comments
 (0)