blob: 19e2f342161627877428986ec304f4c1b079c597 [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
arithmetic1728e115bae2020-05-06 16:00:17 -0700135 def make_request(
136 self,
137 data,
138 status=http_client.OK,
139 headers=None,
140 side_effect=None,
141 use_data_bytes=True,
142 ):
salrashid1231fbc6792018-11-09 11:05:34 -0800143 response = mock.create_autospec(transport.Response, instance=False)
144 response.status = status
arithmetic1728e115bae2020-05-06 16:00:17 -0700145 response.data = _helpers.to_bytes(data) if use_data_bytes else data
salrashid1231fbc6792018-11-09 11:05:34 -0800146 response.headers = headers or {}
147
148 request = mock.create_autospec(transport.Request, instance=False)
149 request.side_effect = side_effect
150 request.return_value = response
151
152 return request
153
arithmetic1728e115bae2020-05-06 16:00:17 -0700154 @pytest.mark.parametrize("use_data_bytes", [True, False])
155 def test_refresh_success(self, use_data_bytes, mock_donor_credentials):
salrashid1231fbc6792018-11-09 11:05:34 -0800156 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700157 token = "token"
salrashid1231fbc6792018-11-09 11:05:34 -0800158
159 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700160 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
161 ).isoformat("T") + "Z"
162 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1231fbc6792018-11-09 11:05:34 -0800163
164 request = self.make_request(
arithmetic1728e115bae2020-05-06 16:00:17 -0700165 data=json.dumps(response_body),
166 status=http_client.OK,
167 use_data_bytes=use_data_bytes,
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700168 )
salrashid1231fbc6792018-11-09 11:05:34 -0800169
170 credentials.refresh(request)
171
172 assert credentials.valid
173 assert not credentials.expired
174
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700175 def test_refresh_failure_malformed_expire_time(self, mock_donor_credentials):
salrashid1231fbc6792018-11-09 11:05:34 -0800176 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700177 token = "token"
salrashid1231fbc6792018-11-09 11:05:34 -0800178
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700179 expire_time = (_helpers.utcnow() + datetime.timedelta(seconds=500)).isoformat(
180 "T"
181 )
182 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1231fbc6792018-11-09 11:05:34 -0800183
184 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700185 data=json.dumps(response_body), status=http_client.OK
186 )
salrashid1231fbc6792018-11-09 11:05:34 -0800187
188 with pytest.raises(exceptions.RefreshError) as excinfo:
189 credentials.refresh(request)
190
191 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
192
193 assert not credentials.valid
194 assert credentials.expired
195
salrashid1231fbc6792018-11-09 11:05:34 -0800196 def test_refresh_failure_unauthorzed(self, mock_donor_credentials):
197 credentials = self.make_credentials(lifetime=None)
198
199 response_body = {
200 "error": {
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700201 "code": 403,
202 "message": "The caller does not have permission",
203 "status": "PERMISSION_DENIED",
salrashid1231fbc6792018-11-09 11:05:34 -0800204 }
205 }
206
207 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700208 data=json.dumps(response_body), status=http_client.UNAUTHORIZED
209 )
salrashid1231fbc6792018-11-09 11:05:34 -0800210
211 with pytest.raises(exceptions.RefreshError) as excinfo:
212 credentials.refresh(request)
213
214 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
215
216 assert not credentials.valid
217 assert credentials.expired
218
219 def test_refresh_failure_http_error(self, mock_donor_credentials):
220 credentials = self.make_credentials(lifetime=None)
221
222 response_body = {}
223
224 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700225 data=json.dumps(response_body), status=http_client.HTTPException
226 )
salrashid1231fbc6792018-11-09 11:05:34 -0800227
228 with pytest.raises(exceptions.RefreshError) as excinfo:
229 credentials.refresh(request)
230
231 assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
232
233 assert not credentials.valid
234 assert credentials.expired
235
236 def test_expired(self):
237 credentials = self.make_credentials(lifetime=None)
238 assert credentials.expired
salrashid1237a8641a2019-08-07 14:31:33 -0700239
240 def test_signer(self):
241 credentials = self.make_credentials()
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700242 assert isinstance(credentials.signer, impersonated_credentials.Credentials)
salrashid1237a8641a2019-08-07 14:31:33 -0700243
244 def test_signer_email(self):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700245 credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
salrashid1237a8641a2019-08-07 14:31:33 -0700246 assert credentials.signer_email == self.TARGET_PRINCIPAL
247
248 def test_service_account_email(self):
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700249 credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL)
salrashid1237a8641a2019-08-07 14:31:33 -0700250 assert credentials.service_account_email == self.TARGET_PRINCIPAL
251
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700252 def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign):
salrashid1237a8641a2019-08-07 14:31:33 -0700253 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700254 token = "token"
salrashid1237a8641a2019-08-07 14:31:33 -0700255
256 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700257 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
258 ).isoformat("T") + "Z"
259 token_response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700260
261 response = mock.create_autospec(transport.Response, instance=False)
262 response.status = http_client.OK
263 response.data = _helpers.to_bytes(json.dumps(token_response_body))
264
265 request = mock.create_autospec(transport.Request, instance=False)
266 request.return_value = response
267
268 credentials.refresh(request)
269
270 assert credentials.valid
271 assert not credentials.expired
272
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700273 signature = credentials.sign_bytes(b"signed bytes")
274 assert signature == b"signature"
salrashid1237a8641a2019-08-07 14:31:33 -0700275
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700276 def test_id_token_success(
277 self, mock_donor_credentials, mock_authorizedsession_idtoken
278 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700279 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700280 token = "token"
281 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700282
283 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700284 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
285 ).isoformat("T") + "Z"
286 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700287
288 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700289 data=json.dumps(response_body), status=http_client.OK
290 )
salrashid1237a8641a2019-08-07 14:31:33 -0700291
292 credentials.refresh(request)
293
294 assert credentials.valid
295 assert not credentials.expired
296
297 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700298 credentials, target_audience=target_audience
299 )
salrashid1237a8641a2019-08-07 14:31:33 -0700300 id_creds.refresh(request)
301
302 assert id_creds.token == ID_TOKEN_DATA
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700303 assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
salrashid1237a8641a2019-08-07 14:31:33 -0700304
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700305 def test_id_token_from_credential(
306 self, mock_donor_credentials, mock_authorizedsession_idtoken
307 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700308 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700309 token = "token"
310 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700311
312 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700313 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
314 ).isoformat("T") + "Z"
315 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700316
317 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700318 data=json.dumps(response_body), status=http_client.OK
319 )
salrashid1237a8641a2019-08-07 14:31:33 -0700320
321 credentials.refresh(request)
322
323 assert credentials.valid
324 assert not credentials.expired
325
326 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700327 credentials, target_audience=target_audience
328 )
salrashid1237a8641a2019-08-07 14:31:33 -0700329 id_creds = id_creds.from_credentials(target_credentials=credentials)
330 id_creds.refresh(request)
331
332 assert id_creds.token == ID_TOKEN_DATA
333
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700334 def test_id_token_with_target_audience(
335 self, mock_donor_credentials, mock_authorizedsession_idtoken
336 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700337 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700338 token = "token"
339 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700340
341 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700342 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
343 ).isoformat("T") + "Z"
344 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700345
346 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700347 data=json.dumps(response_body), status=http_client.OK
348 )
salrashid1237a8641a2019-08-07 14:31:33 -0700349
350 credentials.refresh(request)
351
352 assert credentials.valid
353 assert not credentials.expired
354
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700355 id_creds = impersonated_credentials.IDTokenCredentials(credentials)
356 id_creds = id_creds.with_target_audience(target_audience=target_audience)
salrashid1237a8641a2019-08-07 14:31:33 -0700357 id_creds.refresh(request)
358
359 assert id_creds.token == ID_TOKEN_DATA
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700360 assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
salrashid1237a8641a2019-08-07 14:31:33 -0700361
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700362 def test_id_token_invalid_cred(
363 self, mock_donor_credentials, mock_authorizedsession_idtoken
364 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700365 credentials = None
366
367 with pytest.raises(exceptions.GoogleAuthError) as excinfo:
368 impersonated_credentials.IDTokenCredentials(credentials)
369
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700370 assert excinfo.match("Provided Credential must be" " impersonated_credentials")
salrashid1237a8641a2019-08-07 14:31:33 -0700371
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700372 def test_id_token_with_include_email(
373 self, mock_donor_credentials, mock_authorizedsession_idtoken
374 ):
salrashid1237a8641a2019-08-07 14:31:33 -0700375 credentials = self.make_credentials(lifetime=None)
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700376 token = "token"
377 target_audience = "https://foo.bar"
salrashid1237a8641a2019-08-07 14:31:33 -0700378
379 expire_time = (
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700380 _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
381 ).isoformat("T") + "Z"
382 response_body = {"accessToken": token, "expireTime": expire_time}
salrashid1237a8641a2019-08-07 14:31:33 -0700383
384 request = self.make_request(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700385 data=json.dumps(response_body), status=http_client.OK
386 )
salrashid1237a8641a2019-08-07 14:31:33 -0700387
388 credentials.refresh(request)
389
390 assert credentials.valid
391 assert not credentials.expired
392
393 id_creds = impersonated_credentials.IDTokenCredentials(
Bu Sun Kim9eec0912019-10-21 17:04:21 -0700394 credentials, target_audience=target_audience
395 )
salrashid1237a8641a2019-08-07 14:31:33 -0700396 id_creds = id_creds.with_include_email(True)
397 id_creds.refresh(request)
398
399 assert id_creds.token == ID_TOKEN_DATA