blob: 31075ca8492228c2025b1604c9688ef259d5fdc2 [file] [log] [blame]
salrashid1231fbc6792018-11-09 11:05:34 -08001# Copyright 2018 Google Inc.
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
15import datetime
16import json
17import os
18
19import mock
20import pytest
21from six.moves import http_client
22
23from google.auth import _helpers
24from google.auth import crypt
25from google.auth import exceptions
26from google.auth import impersonated_credentials
27from google.auth import transport
28from google.auth.impersonated_credentials import Credentials
Bu Sun Kim82e224b2020-03-13 13:21:18 -070029from google.oauth2 import credentials
salrashid1231fbc6792018-11-09 11:05:34 -080030from google.oauth2 import service_account
31
Bu Sun Kim9eec0912019-10-21 17:04:21 -070032DATA_DIR = os.path.join(os.path.dirname(__file__), "", "data")
salrashid1231fbc6792018-11-09 11:05:34 -080033
Bu Sun Kim9eec0912019-10-21 17:04:21 -070034with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh:
salrashid1231fbc6792018-11-09 11:05:34 -080035 PRIVATE_KEY_BYTES = fh.read()
36
Bu Sun Kim9eec0912019-10-21 17:04:21 -070037SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json")
salrashid1231fbc6792018-11-09 11:05:34 -080038
Bu Sun Kim9eec0912019-10-21 17:04:21 -070039ID_TOKEN_DATA = (
40 "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNhZDk3N2Ew"
41 "Yjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwc"
42 "zovL2Zvby5iYXIiLCJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJle"
43 "HAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwiaXNzIjoiaHR0cHM6L"
44 "y9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwN"
45 "zA4NTY4In0.redacted"
46)
salrashid1237a8641a2019-08-07 14:31:33 -070047ID_TOKEN_EXPIRY = 1564475051
48
Bu Sun Kim9eec0912019-10-21 17:04:21 -070049with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh:
salrashid1231fbc6792018-11-09 11:05:34 -080050 SERVICE_ACCOUNT_INFO = json.load(fh)
51
Bu Sun Kim9eec0912019-10-21 17:04:21 -070052SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
53TOKEN_URI = "https://example.com/oauth2/token"
salrashid1231fbc6792018-11-09 11:05:34 -080054
55
56@pytest.fixture
57def mock_donor_credentials():
Bu Sun Kim9eec0912019-10-21 17:04:21 -070058 with mock.patch("google.oauth2._client.jwt_grant", autospec=True) as grant:
salrashid1231fbc6792018-11-09 11:05:34 -080059 grant.return_value = (
60 "source token",
61 _helpers.utcnow() + datetime.timedelta(seconds=500),
Bu Sun Kim9eec0912019-10-21 17:04:21 -070062 {},
63 )
salrashid1231fbc6792018-11-09 11:05:34 -080064 yield grant
65
66
salrashid1237a8641a2019-08-07 14:31:33 -070067class MockResponse:
68 def __init__(self, json_data, status_code):
69 self.json_data = json_data
70 self.status_code = status_code
71
72 def json(self):
73 return self.json_data
74
75
76@pytest.fixture
77def mock_authorizedsession_sign():
Bu Sun Kim9eec0912019-10-21 17:04:21 -070078 with mock.patch(
79 "google.auth.transport.requests.AuthorizedSession.request", autospec=True
80 ) as auth_session:
81 data = {"keyId": "1", "signedBlob": "c2lnbmF0dXJl"}
salrashid1237a8641a2019-08-07 14:31:33 -070082 auth_session.return_value = MockResponse(data, http_client.OK)
83 yield auth_session
84
85
86@pytest.fixture
87def mock_authorizedsession_idtoken():
Bu Sun Kim9eec0912019-10-21 17:04:21 -070088 with mock.patch(
89 "google.auth.transport.requests.AuthorizedSession.request", autospec=True
90 ) as auth_session:
91 data = {"token": ID_TOKEN_DATA}
salrashid1237a8641a2019-08-07 14:31:33 -070092 auth_session.return_value = MockResponse(data, http_client.OK)
93 yield auth_session
94
95
salrashid1231fbc6792018-11-09 11:05:34 -080096class TestImpersonatedCredentials(object):
97
Bu Sun Kim9eec0912019-10-21 17:04:21 -070098 SERVICE_ACCOUNT_EMAIL = "service-account@example.com"
99 TARGET_PRINCIPAL = "impersonated@project.iam.gserviceaccount.com"
100 TARGET_SCOPES = ["https://www.googleapis.com/auth/devstorage.read_only"]
salrashid1231fbc6792018-11-09 11:05:34 -0800101 DELEGATES = []
102 LIFETIME = 3600
103 SOURCE_CREDENTIALS = service_account.Credentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700104 SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI
105 )
Bu Sun Kim82e224b2020-03-13 13:21:18 -0700106 USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE")
salrashid1231fbc6792018-11-09 11:05:34 -0800107
Bu Sun Kim82e224b2020-03-13 13:21:18 -0700108 def make_credentials(
109 self,
110 source_credentials=SOURCE_CREDENTIALS,
111 lifetime=LIFETIME,
112 target_principal=TARGET_PRINCIPAL,
113 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700114
salrashid1231fbc6792018-11-09 11:05:34 -0800115 return Credentials(
Bu Sun Kim82e224b2020-03-13 13:21:18 -0700116 source_credentials=source_credentials,
salrashid1237a8641a2019-08-07 14:31:33 -0700117 target_principal=target_principal,
salrashid1231fbc6792018-11-09 11:05:34 -0800118 target_scopes=self.TARGET_SCOPES,
119 delegates=self.DELEGATES,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700120 lifetime=lifetime,
121 )
salrashid1231fbc6792018-11-09 11:05:34 -0800122
Bu Sun Kim82e224b2020-03-13 13:21:18 -0700123 def test_make_from_user_credentials(self):
124 credentials = self.make_credentials(
125 source_credentials=self.USER_SOURCE_CREDENTIALS
126 )
127 assert not credentials.valid
128 assert credentials.expired
129
salrashid1231fbc6792018-11-09 11:05:34 -0800130 def test_default_state(self):
131 credentials = self.make_credentials()
132 assert not credentials.valid
133 assert credentials.expired
134
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700135 def make_request(self, data, status=http_client.OK, headers=None, side_effect=None):
salrashid1231fbc6792018-11-09 11:05:34 -0800136 response = mock.create_autospec(transport.Response, instance=False)
137 response.status = status
138 response.data = _helpers.to_bytes(data)
139 response.headers = headers or {}
140
141 request = mock.create_autospec(transport.Request, instance=False)
142 request.side_effect = side_effect
143 request.return_value = response
144
145 return request
146
147 def test_refresh_success(self, mock_donor_credentials):
148 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700149 token = "token"
salrashid1231fbc6792018-11-09 11:05:34 -0800150
151 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700152 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
153 ).isoformat("T") + "Z"
154 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1231fbc6792018-11-09 11:05:34 -0800155
156 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700157 data=json.dumps(response_body), status=http_client.OK
158 )
salrashid1231fbc6792018-11-09 11:05:34 -0800159
160 credentials.refresh(request)
161
162 assert credentials.valid
163 assert not credentials.expired
164
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700165 def test_refresh_failure_malformed_expire_time(self, mock_donor_credentials):
salrashid1231fbc6792018-11-09 11:05:34 -0800166 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700167 token = "token"
salrashid1231fbc6792018-11-09 11:05:34 -0800168
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700169 expire_time = (_helpers.utcnow() + datetime.timedelta(seconds=500)).isoformat(
170 "T"
171 )
172 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1231fbc6792018-11-09 11:05:34 -0800173
174 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700175 data=json.dumps(response_body), status=http_client.OK
176 )
salrashid1231fbc6792018-11-09 11:05:34 -0800177
178 with pytest.raises(exceptions.RefreshError) as excinfo:
179 credentials.refresh(request)
180
181 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
182
183 assert not credentials.valid
184 assert credentials.expired
185
salrashid1231fbc6792018-11-09 11:05:34 -0800186 def test_refresh_failure_unauthorzed(self, mock_donor_credentials):
187 credentials = self.make_credentials(lifetime=None)
188
189 response_body = {
190 "error": {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700191 "code": 403,
192 "message": "The caller does not have permission",
193 "status": "PERMISSION_DENIED",
salrashid1231fbc6792018-11-09 11:05:34 -0800194 }
195 }
196
197 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700198 data=json.dumps(response_body), status=http_client.UNAUTHORIZED
199 )
salrashid1231fbc6792018-11-09 11:05:34 -0800200
201 with pytest.raises(exceptions.RefreshError) as excinfo:
202 credentials.refresh(request)
203
204 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
205
206 assert not credentials.valid
207 assert credentials.expired
208
209 def test_refresh_failure_http_error(self, mock_donor_credentials):
210 credentials = self.make_credentials(lifetime=None)
211
212 response_body = {}
213
214 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700215 data=json.dumps(response_body), status=http_client.HTTPException
216 )
salrashid1231fbc6792018-11-09 11:05:34 -0800217
218 with pytest.raises(exceptions.RefreshError) as excinfo:
219 credentials.refresh(request)
220
221 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
222
223 assert not credentials.valid
224 assert credentials.expired
225
226 def test_expired(self):
227 credentials = self.make_credentials(lifetime=None)
228 assert credentials.expired
salrashid1237a8641a2019-08-07 14:31:33 -0700229
230 def test_signer(self):
231 credentials = self.make_credentials()
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700232 assert isinstance(credentials.signer, impersonated_credentials.Credentials)
salrashid1237a8641a2019-08-07 14:31:33 -0700233
234 def test_signer_email(self):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700235 credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
salrashid1237a8641a2019-08-07 14:31:33 -0700236 assert credentials.signer_email == self.TARGET_PRINCIPAL
237
238 def test_service_account_email(self):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700239 credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
salrashid1237a8641a2019-08-07 14:31:33 -0700240 assert credentials.service_account_email == self.TARGET_PRINCIPAL
241
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign):
salrashid1237a8641a2019-08-07 14:31:33 -0700243 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700244 token = "token"
salrashid1237a8641a2019-08-07 14:31:33 -0700245
246 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700247 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
248 ).isoformat("T") + "Z"
249 token_response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700250
251 response = mock.create_autospec(transport.Response, instance=False)
252 response.status = http_client.OK
253 response.data = _helpers.to_bytes(json.dumps(token_response_body))
254
255 request = mock.create_autospec(transport.Request, instance=False)
256 request.return_value = response
257
258 credentials.refresh(request)
259
260 assert credentials.valid
261 assert not credentials.expired
262
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700263 signature = credentials.sign_bytes(b"signed bytes")
264 assert signature == b"signature"
salrashid1237a8641a2019-08-07 14:31:33 -0700265
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700266 def test_id_token_success(
267 self, mock_donor_credentials, mock_authorizedsession_idtoken
268 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700269 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700270 token = "token"
271 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700272
273 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700274 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
275 ).isoformat("T") + "Z"
276 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700277
278 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700279 data=json.dumps(response_body), status=http_client.OK
280 )
salrashid1237a8641a2019-08-07 14:31:33 -0700281
282 credentials.refresh(request)
283
284 assert credentials.valid
285 assert not credentials.expired
286
287 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700288 credentials, target_audience=target_audience
289 )
salrashid1237a8641a2019-08-07 14:31:33 -0700290 id_creds.refresh(request)
291
292 assert id_creds.token == ID_TOKEN_DATA
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700293 assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
salrashid1237a8641a2019-08-07 14:31:33 -0700294
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700295 def test_id_token_from_credential(
296 self, mock_donor_credentials, mock_authorizedsession_idtoken
297 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700298 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700299 token = "token"
300 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700301
302 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700303 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
304 ).isoformat("T") + "Z"
305 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700306
307 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700308 data=json.dumps(response_body), status=http_client.OK
309 )
salrashid1237a8641a2019-08-07 14:31:33 -0700310
311 credentials.refresh(request)
312
313 assert credentials.valid
314 assert not credentials.expired
315
316 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700317 credentials, target_audience=target_audience
318 )
salrashid1237a8641a2019-08-07 14:31:33 -0700319 id_creds = id_creds.from_credentials(target_credentials=credentials)
320 id_creds.refresh(request)
321
322 assert id_creds.token == ID_TOKEN_DATA
323
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700324 def test_id_token_with_target_audience(
325 self, mock_donor_credentials, mock_authorizedsession_idtoken
326 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700327 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700328 token = "token"
329 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700330
331 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700332 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
333 ).isoformat("T") + "Z"
334 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700335
336 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700337 data=json.dumps(response_body), status=http_client.OK
338 )
salrashid1237a8641a2019-08-07 14:31:33 -0700339
340 credentials.refresh(request)
341
342 assert credentials.valid
343 assert not credentials.expired
344
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700345 id_creds = impersonated_credentials.IDTokenCredentials(credentials)
346 id_creds = id_creds.with_target_audience(target_audience=target_audience)
salrashid1237a8641a2019-08-07 14:31:33 -0700347 id_creds.refresh(request)
348
349 assert id_creds.token == ID_TOKEN_DATA
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700350 assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
salrashid1237a8641a2019-08-07 14:31:33 -0700351
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700352 def test_id_token_invalid_cred(
353 self, mock_donor_credentials, mock_authorizedsession_idtoken
354 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700355 credentials = None
356
357 with pytest.raises(exceptions.GoogleAuthError) as excinfo:
358 impersonated_credentials.IDTokenCredentials(credentials)
359
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700360 assert excinfo.match("Provided Credential must be" " impersonated_credentials")
salrashid1237a8641a2019-08-07 14:31:33 -0700361
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700362 def test_id_token_with_include_email(
363 self, mock_donor_credentials, mock_authorizedsession_idtoken
364 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700365 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700366 token = "token"
367 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700368
369 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700370 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
371 ).isoformat("T") + "Z"
372 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700373
374 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700375 data=json.dumps(response_body), status=http_client.OK
376 )
salrashid1237a8641a2019-08-07 14:31:33 -0700377
378 credentials.refresh(request)
379
380 assert credentials.valid
381 assert not credentials.expired
382
383 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700384 credentials, target_audience=target_audience
385 )
salrashid1237a8641a2019-08-07 14:31:33 -0700386 id_creds = id_creds.with_include_email(True)
387 id_creds.refresh(request)
388
389 assert id_creds.token == ID_TOKEN_DATA