Skip to content

Commit a4d432a

Browse files
seanzhougooglecopybara-github
authored andcommitted
chore: Add Service Account Credential Exchanger (Experimental)
PiperOrigin-RevId: 771507089
1 parent d1bda9d commit a4d432a

File tree

2 files changed

+433
-0
lines changed

2 files changed

+433
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Credential fetcher for Google Service Account."""
16+
17+
from __future__ import annotations
18+
19+
import google.auth
20+
from google.auth.transport.requests import Request
21+
from google.oauth2 import service_account
22+
23+
from ..utils.feature_decorator import experimental
24+
from .auth_credential import AuthCredential
25+
from .auth_credential import AuthCredentialTypes
26+
from .auth_credential import HttpAuth
27+
from .auth_credential import HttpCredentials
28+
29+
30+
@experimental
31+
class ServiceAccountCredentialExchanger:
32+
"""Exchanges Google Service Account credentials for an access token.
33+
34+
Uses the default service credential if `use_default_credential = True`.
35+
Otherwise, uses the service account credential provided in the auth
36+
credential.
37+
"""
38+
39+
def __init__(self, credential: AuthCredential):
40+
if credential.auth_type != AuthCredentialTypes.SERVICE_ACCOUNT:
41+
raise ValueError("Credential is not a service account credential.")
42+
self._credential = credential
43+
44+
def exchange(self) -> AuthCredential:
45+
"""Exchanges the service account auth credential for an access token.
46+
47+
If the AuthCredential contains a service account credential, it will be used
48+
to exchange for an access token. Otherwise, if use_default_credential is True,
49+
the default application credential will be used for exchanging an access token.
50+
51+
Returns:
52+
An AuthCredential in HTTP Bearer format, containing the access token.
53+
54+
Raises:
55+
ValueError: If service account credentials are missing or invalid.
56+
Exception: If credential exchange or refresh fails.
57+
"""
58+
if (
59+
self._credential is None
60+
or self._credential.service_account is None
61+
or (
62+
self._credential.service_account.service_account_credential is None
63+
and not self._credential.service_account.use_default_credential
64+
)
65+
):
66+
raise ValueError(
67+
"Service account credentials are missing. Please provide them, or set"
68+
" `use_default_credential = True` to use application default"
69+
" credential in a hosted service like Google Cloud Run."
70+
)
71+
72+
try:
73+
if self._credential.service_account.use_default_credential:
74+
credentials, _ = google.auth.default()
75+
else:
76+
config = self._credential.service_account
77+
credentials = service_account.Credentials.from_service_account_info(
78+
config.service_account_credential.model_dump(), scopes=config.scopes
79+
)
80+
81+
# Refresh credentials to ensure we have a valid access token
82+
credentials.refresh(Request())
83+
84+
return AuthCredential(
85+
auth_type=AuthCredentialTypes.HTTP,
86+
http=HttpAuth(
87+
scheme="bearer",
88+
credentials=HttpCredentials(token=credentials.token),
89+
),
90+
)
91+
except Exception as e:
92+
raise ValueError(f"Failed to exchange service account token: {e}") from e

0 commit comments

Comments
 (0)