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

Commit d696ce3

Browse files
committed
feat: support self-signed JWT flow for servie accounts
1 parent 9bde5c7 commit d696ce3

5 files changed

Lines changed: 307 additions & 23 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ class {{ service.name }}Transport(abc.ABC):
4343
{%- endfor %}
4444
)
4545

46+
DEFAULT_HOST = {% if service.host %}'{{ service.host }}'{% else %}{{ None }}{% endif %}
47+
4648
def __init__(
4749
self, *,
48-
host: str{% if service.host %} = '{{ service.host }}'{% endif %},
50+
host: str = DEFAULT_HOST,
4951
credentials: credentials.Credentials = None,
5052
credentials_file: typing.Optional[str] = None,
5153
scopes: typing.Optional[typing.Sequence[str]] = AUTH_SCOPES,

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

Lines changed: 48 additions & 6 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

@@ -27,6 +30,17 @@ from google.iam.v1 import policy_pb2 as policy # type: ignore
2730
{% endfilter %}
2831
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
2932

33+
try:
34+
# google.auth.__version__ was added in 1.26.0
35+
_GOOGLE_AUTH_VERSION = auth.__version__
36+
except AttributeError:
37+
try: # try pkg_resources if it is available
38+
_GOOGLE_AUTH_VERSION = pkg_resources.get_distribution("google-auth").version
39+
except pkg_resources.DistributionNotFound: # pragma: NO COVER
40+
_GOOGLE_AUTH_VERSION = None
41+
42+
_API_CORE_VERSION = google.api_core.__version__
43+
3044

3145
class {{ service.name }}GrpcTransport({{ service.name }}Transport):
3246
"""gRPC backend transport for {{ service.name }}.
@@ -101,6 +115,22 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
101115
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
102116
and ``credentials_file`` are passed.
103117
"""
118+
119+
# If a custom API endpoint is set, set scopes to ensure the auth
120+
# library does not used the self-signed JWT flow for service
121+
# accounts
122+
if host.split(":")[0] != self.DEFAULT_HOST and not scopes:
123+
scopes = self.AUTH_SCOPES
124+
125+
# TODO(busunkim): Remove this if/else once google-auth >= 1.25.0 is required
126+
if _GOOGLE_AUTH_VERSION and (
127+
packaging.version.parse(_GOOGLE_AUTH_VERSION)
128+
>= packaging.version.parse("1.25.0")
129+
):
130+
scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES}
131+
else:
132+
scopes_kwargs = {"scopes": scopes or self.AUTH_SCOPES}
133+
104134
self._ssl_channel_credentials = ssl_channel_credentials
105135

106136
if api_mtls_endpoint:
@@ -120,7 +150,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
120150
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
121151

122152
if credentials is None:
123-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
153+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
124154

125155
# Create SSL credentials with client_cert_source or application
126156
# default SSL credentials.
@@ -138,7 +168,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
138168
credentials=credentials,
139169
credentials_file=credentials_file,
140170
ssl_credentials=ssl_credentials,
141-
scopes=scopes or self.AUTH_SCOPES,
171+
scopes=scopes,
142172
quota_project_id=quota_project_id,
143173
options=[
144174
("grpc.max_send_message_length", -1),
@@ -150,7 +180,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
150180
host = host if ":" in host else host + ":443"
151181

152182
if credentials is None:
153-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
183+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
154184

155185
if client_cert_source_for_mtls and not ssl_channel_credentials:
156186
cert, key = client_cert_source_for_mtls()
@@ -164,7 +194,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
164194
credentials=credentials,
165195
credentials_file=credentials_file,
166196
ssl_credentials=self._ssl_channel_credentials,
167-
scopes=scopes or self.AUTH_SCOPES,
197+
scopes=scopes,
168198
quota_project_id=quota_project_id,
169199
options=[
170200
("grpc.max_send_message_length", -1),
@@ -182,7 +212,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
182212
host=host,
183213
credentials=credentials,
184214
credentials_file=credentials_file,
185-
scopes=scopes or self.AUTH_SCOPES,
215+
scopes=scopes,
186216
quota_project_id=quota_project_id,
187217
client_info=client_info,
188218
)
@@ -220,7 +250,19 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
220250
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
221251
and ``credentials_file`` are passed.
222252
"""
223-
scopes = scopes or cls.AUTH_SCOPES
253+
self_signed_jwt_kwargs = {}
254+
255+
# TODO(busunkim): Remove this if/else once google-api-core >= 1.26.0 is required
256+
if _API_CORE_VERSION and (
257+
packaging.version.parse(_API_CORE_VERSION)
258+
>= packaging.version.parse("1.26.0")
259+
):
260+
self_signed_jwt_kwargs["default_scopes"] = cls.AUTH_SCOPES
261+
self_signed_jwt_kwargs["scopes"] = scopes
262+
self_signed_jwt_kwargs["default_host"] = cls.DEFAULT_HOST
263+
else:
264+
self_signed_jwt_kwargs["scopes"] = scopes or cls.AUTH_SCOPES
265+
224266
return grpc_helpers.create_channel(
225267
host,
226268
credentials=credentials,

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

Lines changed: 33 additions & 3 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
@@ -28,6 +29,8 @@ from google.iam.v1 import policy_pb2 as policy # type: ignore
2829
{% endfilter %}
2930
from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO
3031
from .grpc import {{ service.name }}GrpcTransport
32+
from .grpc import _API_CORE_VERSION
33+
from .grpc import _GOOGLE_AUTH_VERSION
3134

3235

3336
class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
@@ -75,7 +78,19 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
7578
Returns:
7679
aio.Channel: A gRPC AsyncIO channel object.
7780
"""
78-
scopes = scopes or cls.AUTH_SCOPES
81+
self_signed_jwt_kwargs = {}
82+
83+
# TODO(busunkim): Remove this if/else once google-api-core >= 1.26.0 is required
84+
if _API_CORE_VERSION and (
85+
packaging.version.parse(_API_CORE_VERSION)
86+
>= packaging.version.parse("1.26.0")
87+
):
88+
self_signed_jwt_kwargs["default_scopes"] = cls.AUTH_SCOPES
89+
self_signed_jwt_kwargs["scopes"] = scopes
90+
self_signed_jwt_kwargs["default_host"] = cls.DEFAULT_HOST
91+
else:
92+
self_signed_jwt_kwargs["scopes"] = scopes or cls.AUTH_SCOPES
93+
7994
return grpc_helpers_async.create_channel(
8095
host,
8196
credentials=credentials,
@@ -145,6 +160,21 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
145160
google.api_core.exceptions.DuplicateCredentialArgs: If both ``credentials``
146161
and ``credentials_file`` are passed.
147162
"""
163+
# If a custom API endpoint is set, set scopes to ensure the auth
164+
# library does not used the self-signed JWT flow for service
165+
# accounts
166+
if host.split(":")[0] != self.DEFAULT_HOST and not scopes:
167+
scopes = self.AUTH_SCOPES
168+
169+
# TODO: Remove this if/else once google-auth >= 1.25.0 is required
170+
if _GOOGLE_AUTH_VERSION and packaging.version.parse(
171+
_GOOGLE_AUTH_VERSION
172+
) >= packaging.version.parse("1.25.0"):
173+
scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES}
174+
else:
175+
scopes_kwargs = {"scopes": scopes or self.AUTH_SCOPES}
176+
177+
148178
self._ssl_channel_credentials = ssl_channel_credentials
149179

150180
if api_mtls_endpoint:
@@ -164,7 +194,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
164194
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
165195

166196
if credentials is None:
167-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
197+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
168198

169199
# Create SSL credentials with client_cert_source or application
170200
# default SSL credentials.
@@ -194,7 +224,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
194224
host = host if ":" in host else host + ":443"
195225

196226
if credentials is None:
197-
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
227+
credentials, _ = auth.default(**scopes_kwargs, quota_project_id=quota_project_id)
198228

199229
if client_cert_source_for_mtls and not ssl_channel_credentials:
200230
cert, key = client_cert_source_for_mtls()

gapic/templates/setup.py.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ setuptools.setup(
2929
'google-api-core[grpc] >= 1.22.2, < 2.0.0dev',
3030
'libcst >= 0.2.5',
3131
'proto-plus >= 1.4.0',
32+
'packaging >= 14.3',
3233
{%- if api.requires_package(('google', 'iam', 'v1')) or opts.add_iam_methods %}
3334
'grpc-google-iam-v1',
3435
{%- endif %}

0 commit comments

Comments
 (0)