Skip to content

Commit 3056f5c

Browse files
feat: add access token credentials (#476)
feat: add access token credentials
1 parent 8ada9dc commit 3056f5c

File tree

7 files changed

+158
-33
lines changed

7 files changed

+158
-33
lines changed

packages/google-auth/google/auth/_cloud_sdk.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import os
1919
import subprocess
2020

21+
import six
22+
2123
from google.auth import environment_vars
22-
import google.oauth2.credentials
24+
from google.auth import exceptions
2325

2426

2527
# The ~/.config subdirectory containing gcloud credentials.
@@ -34,6 +36,8 @@
3436
_CLOUD_SDK_WINDOWS_COMMAND = "gcloud.cmd"
3537
# The command to get the Cloud SDK configuration
3638
_CLOUD_SDK_CONFIG_COMMAND = ("config", "config-helper", "--format", "json")
39+
# The command to get google user access token
40+
_CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND = ("auth", "print-access-token")
3741
# Cloud SDK's application-default client ID
3842
CLOUD_SDK_CLIENT_ID = (
3943
"764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com"
@@ -80,21 +84,6 @@ def get_application_default_credentials_path():
8084
return os.path.join(config_path, _CREDENTIALS_FILENAME)
8185

8286

83-
def load_authorized_user_credentials(info):
84-
"""Loads an authorized user credential.
85-
86-
Args:
87-
info (Mapping[str, str]): The loaded file's data.
88-
89-
Returns:
90-
google.oauth2.credentials.Credentials: The constructed credentials.
91-
92-
Raises:
93-
ValueError: if the info is in the wrong format or missing data.
94-
"""
95-
return google.oauth2.credentials.Credentials.from_authorized_user_info(info)
96-
97-
9887
def get_project_id():
9988
"""Gets the project ID from the Cloud SDK.
10089
@@ -122,3 +111,42 @@ def get_project_id():
122111
return configuration["configuration"]["properties"]["core"]["project"]
123112
except KeyError:
124113
return None
114+
115+
116+
def get_auth_access_token(account=None):
117+
"""Load user access token with the ``gcloud auth print-access-token`` command.
118+
119+
Args:
120+
account (Optional[str]): Account to get the access token for. If not
121+
specified, the current active account will be used.
122+
123+
Returns:
124+
str: The user access token.
125+
126+
Raises:
127+
google.auth.exceptions.UserAccessTokenError: if failed to get access
128+
token from gcloud.
129+
"""
130+
if os.name == "nt":
131+
command = _CLOUD_SDK_WINDOWS_COMMAND
132+
else:
133+
command = _CLOUD_SDK_POSIX_COMMAND
134+
135+
try:
136+
if account:
137+
command = (
138+
(command,)
139+
+ _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
140+
+ ("--account=" + account,)
141+
)
142+
else:
143+
command = (command,) + _CLOUD_SDK_USER_ACCESS_TOKEN_COMMAND
144+
145+
access_token = subprocess.check_output(command, stderr=subprocess.STDOUT)
146+
# remove the trailing "\n"
147+
return access_token.decode("utf-8").strip()
148+
except (subprocess.CalledProcessError, OSError, IOError) as caught_exc:
149+
new_exc = exceptions.UserAccessTokenError(
150+
"Failed to obtain access token", caught_exc
151+
)
152+
six.raise_from(new_exc, caught_exc)

packages/google-auth/google/auth/_default.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ def _load_credentials_from_file(filename):
106106
credential_type = info.get("type")
107107

108108
if credential_type == _AUTHORIZED_USER_TYPE:
109-
from google.auth import _cloud_sdk
109+
from google.oauth2 import credentials
110110

111111
try:
112-
credentials = _cloud_sdk.load_authorized_user_credentials(info)
112+
credentials = credentials.Credentials.from_authorized_user_info(info)
113113
except ValueError as caught_exc:
114114
msg = "Failed to load authorized user credentials from {}".format(filename)
115115
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)

packages/google-auth/google/auth/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,9 @@ class RefreshError(GoogleAuthError):
2828
failed."""
2929

3030

31+
class UserAccessTokenError(GoogleAuthError):
32+
"""Used to indicate ``gcloud auth print-access-token`` command failed."""
33+
34+
3135
class DefaultCredentialsError(GoogleAuthError):
3236
"""Used to indicate that acquiring default credentials failed."""

packages/google-auth/google/oauth2/credentials.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
import six
3838

39+
from google.auth import _cloud_sdk
3940
from google.auth import _helpers
4041
from google.auth import credentials
4142
from google.auth import exceptions
@@ -292,3 +293,50 @@ def to_json(self, strip=None):
292293
prep = {k: v for k, v in prep.items() if k not in strip}
293294

294295
return json.dumps(prep)
296+
297+
298+
class UserAccessTokenCredentials(credentials.Credentials):
299+
"""Access token credentials for user account.
300+
301+
Obtain the access token for a given user account or the current active
302+
user account with the ``gcloud auth print-access-token`` command.
303+
304+
Args:
305+
account (Optional[str]): Account to get the access token for. If not
306+
specified, the current active account will be used.
307+
"""
308+
309+
def __init__(self, account=None):
310+
super(UserAccessTokenCredentials, self).__init__()
311+
self._account = account
312+
313+
def with_account(self, account):
314+
"""Create a new instance with the given account.
315+
316+
Args:
317+
account (str): Account to get the access token for.
318+
319+
Returns:
320+
google.oauth2.credentials.UserAccessTokenCredentials: The created
321+
credentials with the given account.
322+
"""
323+
return self.__class__(account=account)
324+
325+
def refresh(self, request):
326+
"""Refreshes the access token.
327+
328+
Args:
329+
request (google.auth.transport.Request): This argument is required
330+
by the base class interface but not used in this implementation,
331+
so just set it to `None`.
332+
333+
Raises:
334+
google.auth.exceptions.UserAccessTokenError: If the access token
335+
refresh failed.
336+
"""
337+
self.token = _cloud_sdk.get_auth_access_token(self._account)
338+
339+
@_helpers.copy_docstring(credentials.Credentials)
340+
def before_request(self, request, method, url, headers):
341+
self.refresh(request)
342+
self.apply(headers)

packages/google-auth/system_tests/test_mtls_http.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import json
1616
from os import path
17+
import time
1718

1819
import google.auth
1920
import google.auth.credentials
@@ -42,6 +43,9 @@ def test_requests():
4243
# supposed to be created.
4344
assert authed_session.is_mtls == check_context_aware_metadata()
4445

46+
# Sleep 1 second to avoid 503 error.
47+
time.sleep(1)
48+
4549
if authed_session.is_mtls:
4650
response = authed_session.get(MTLS_ENDPOINT.format(project_id))
4751
else:
@@ -63,6 +67,9 @@ def test_urllib3():
6367
# supposed to be created.
6468
assert is_mtls == check_context_aware_metadata()
6569

70+
# Sleep 1 second to avoid 503 error.
71+
time.sleep(1)
72+
6673
if is_mtls:
6774
response = authed_http.request("GET", MTLS_ENDPOINT.format(project_id))
6875
else:

packages/google-auth/tests/oauth2/test_credentials.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,3 +421,31 @@ def test_unpickle_old_credentials_pickle(self):
421421
) as f:
422422
credentials = pickle.load(f)
423423
assert credentials.quota_project_id is None
424+
425+
426+
class TestUserAccessTokenCredentials(object):
427+
def test_instance(self):
428+
cred = credentials.UserAccessTokenCredentials()
429+
assert cred._account is None
430+
431+
cred = cred.with_account("account")
432+
assert cred._account == "account"
433+
434+
@mock.patch("google.auth._cloud_sdk.get_auth_access_token", autospec=True)
435+
def test_refresh(self, get_auth_access_token):
436+
get_auth_access_token.return_value = "access_token"
437+
cred = credentials.UserAccessTokenCredentials()
438+
cred.refresh(None)
439+
assert cred.token == "access_token"
440+
441+
@mock.patch(
442+
"google.oauth2.credentials.UserAccessTokenCredentials.apply", autospec=True
443+
)
444+
@mock.patch(
445+
"google.oauth2.credentials.UserAccessTokenCredentials.refresh", autospec=True
446+
)
447+
def test_before_request(self, refresh, apply):
448+
cred = credentials.UserAccessTokenCredentials()
449+
cred.before_request(mock.Mock(), "GET", "https://example.com", {})
450+
refresh.assert_called()
451+
apply.assert_called()

packages/google-auth/tests/test__cloud_sdk.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from google.auth import _cloud_sdk
2424
from google.auth import environment_vars
25-
import google.oauth2.credentials
25+
from google.auth import exceptions
2626

2727

2828
DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
@@ -137,23 +137,33 @@ def test_get_config_path_no_appdata(monkeypatch):
137137
assert os.path.split(config_path) == ("G:/\\", _cloud_sdk._CONFIG_DIRECTORY)
138138

139139

140-
def test_load_authorized_user_credentials():
141-
credentials = _cloud_sdk.load_authorized_user_credentials(AUTHORIZED_USER_FILE_DATA)
140+
@mock.patch("os.name", new="nt")
141+
@mock.patch("subprocess.check_output", autospec=True)
142+
def test_get_auth_access_token_windows(check_output):
143+
check_output.return_value = b"access_token\n"
144+
145+
token = _cloud_sdk.get_auth_access_token()
146+
assert token == "access_token"
147+
check_output.assert_called_with(
148+
("gcloud.cmd", "auth", "print-access-token"), stderr=subprocess.STDOUT
149+
)
150+
142151

143-
assert isinstance(credentials, google.oauth2.credentials.Credentials)
152+
@mock.patch("subprocess.check_output", autospec=True)
153+
def test_get_auth_access_token_with_account(check_output):
154+
check_output.return_value = b"access_token\n"
144155

145-
assert credentials.token is None
146-
assert credentials._refresh_token == AUTHORIZED_USER_FILE_DATA["refresh_token"]
147-
assert credentials._client_id == AUTHORIZED_USER_FILE_DATA["client_id"]
148-
assert credentials._client_secret == AUTHORIZED_USER_FILE_DATA["client_secret"]
149-
assert (
150-
credentials._token_uri
151-
== google.oauth2.credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT
156+
token = _cloud_sdk.get_auth_access_token(account="account")
157+
assert token == "access_token"
158+
check_output.assert_called_with(
159+
("gcloud", "auth", "print-access-token", "--account=account"),
160+
stderr=subprocess.STDOUT,
152161
)
153162

154163

155-
def test_load_authorized_user_credentials_bad_format():
156-
with pytest.raises(ValueError) as excinfo:
157-
_cloud_sdk.load_authorized_user_credentials({})
164+
@mock.patch("subprocess.check_output", autospec=True)
165+
def test_get_auth_access_token_with_exception(check_output):
166+
check_output.side_effect = OSError()
158167

159-
assert excinfo.match(r"missing fields")
168+
with pytest.raises(exceptions.UserAccessTokenError):
169+
_cloud_sdk.get_auth_access_token(account="account")

0 commit comments

Comments
 (0)