test_credentials.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940
  1. # Copyright 2016 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. import base64
  15. import datetime
  16. import mock
  17. import pytest # type: ignore
  18. import responses # type: ignore
  19. from google.auth import _helpers
  20. from google.auth import exceptions
  21. from google.auth import jwt
  22. from google.auth import transport
  23. from google.auth.compute_engine import credentials
  24. from google.auth.transport import requests
  25. SAMPLE_ID_TOKEN_EXP = 1584393400
  26. # header: {"alg": "RS256", "typ": "JWT", "kid": "1"}
  27. # payload: {"iss": "issuer", "iat": 1584393348, "sub": "subject",
  28. # "exp": 1584393400,"aud": "audience"}
  29. SAMPLE_ID_TOKEN = (
  30. b"eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiMSJ9."
  31. b"eyJpc3MiOiAiaXNzdWVyIiwgImlhdCI6IDE1ODQzOTMzNDgsICJzdWIiO"
  32. b"iAic3ViamVjdCIsICJleHAiOiAxNTg0MzkzNDAwLCAiYXVkIjogImF1ZG"
  33. b"llbmNlIn0."
  34. b"OquNjHKhTmlgCk361omRo18F_uY-7y0f_AmLbzW062Q1Zr61HAwHYP5FM"
  35. b"316CK4_0cH8MUNGASsvZc3VqXAqub6PUTfhemH8pFEwBdAdG0LhrNkU0H"
  36. b"WN1YpT55IiQ31esLdL5q-qDsOPpNZJUti1y1lAreM5nIn2srdWzGXGs4i"
  37. b"TRQsn0XkNUCL4RErpciXmjfhMrPkcAjKA-mXQm2fa4jmTlEZFqFmUlym1"
  38. b"ozJ0yf5grjN6AslN4OGvAv1pS-_Ko_pGBS6IQtSBC6vVKCUuBfaqNjykg"
  39. b"bsxbLa6Fp0SYeYwO8ifEnkRvasVpc1WTQqfRB2JCj5pTBDzJpIpFCMmnQ"
  40. )
  41. ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
  42. "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
  43. )
  44. ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
  45. "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds"
  46. )
  47. FAKE_SERVICE_ACCOUNT_EMAIL = "foo@bar.com"
  48. FAKE_QUOTA_PROJECT_ID = "fake-quota-project"
  49. FAKE_SCOPES = ["scope1", "scope2"]
  50. FAKE_DEFAULT_SCOPES = ["scope3", "scope4"]
  51. FAKE_UNIVERSE_DOMAIN = "fake-universe-domain"
  52. class TestCredentials(object):
  53. credentials = None
  54. credentials_with_all_fields = None
  55. @pytest.fixture(autouse=True)
  56. def credentials_fixture(self):
  57. self.credentials = credentials.Credentials()
  58. self.credentials_with_all_fields = credentials.Credentials(
  59. service_account_email=FAKE_SERVICE_ACCOUNT_EMAIL,
  60. quota_project_id=FAKE_QUOTA_PROJECT_ID,
  61. scopes=FAKE_SCOPES,
  62. default_scopes=FAKE_DEFAULT_SCOPES,
  63. universe_domain=FAKE_UNIVERSE_DOMAIN,
  64. )
  65. def test_default_state(self):
  66. assert not self.credentials.valid
  67. # Expiration hasn't been set yet
  68. assert not self.credentials.expired
  69. # Scopes are needed
  70. assert self.credentials.requires_scopes
  71. # Service account email hasn't been populated
  72. assert self.credentials.service_account_email == "default"
  73. # No quota project
  74. assert not self.credentials._quota_project_id
  75. # Universe domain is the default and not cached
  76. assert self.credentials._universe_domain == "googleapis.com"
  77. assert not self.credentials._universe_domain_cached
  78. @mock.patch(
  79. "google.auth._helpers.utcnow",
  80. return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
  81. )
  82. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  83. def test_refresh_success(self, get, utcnow):
  84. get.side_effect = [
  85. {
  86. # First request is for sevice account info.
  87. "email": "service-account@example.com",
  88. "scopes": ["one", "two"],
  89. },
  90. {
  91. # Second request is for the token.
  92. "access_token": "token",
  93. "expires_in": 500,
  94. },
  95. ]
  96. # Refresh credentials
  97. self.credentials.refresh(None)
  98. # Check that the credentials have the token and proper expiration
  99. assert self.credentials.token == "token"
  100. assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
  101. # Check the credential info
  102. assert self.credentials.service_account_email == "service-account@example.com"
  103. assert self.credentials._scopes == ["one", "two"]
  104. # Check that the credentials are valid (have a token and are not
  105. # expired)
  106. assert self.credentials.valid
  107. @mock.patch(
  108. "google.auth.metrics.token_request_access_token_mds",
  109. return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
  110. )
  111. @mock.patch(
  112. "google.auth._helpers.utcnow",
  113. return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
  114. )
  115. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  116. def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_value):
  117. get.side_effect = [
  118. {
  119. # First request is for sevice account info.
  120. "email": "service-account@example.com",
  121. "scopes": ["one", "two"],
  122. },
  123. {
  124. # Second request is for the token.
  125. "access_token": "token",
  126. "expires_in": 500,
  127. },
  128. ]
  129. # Refresh credentials
  130. scopes = ["three", "four"]
  131. self.credentials = self.credentials.with_scopes(scopes)
  132. self.credentials.refresh(None)
  133. # Check that the credentials have the token and proper expiration
  134. assert self.credentials.token == "token"
  135. assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
  136. # Check the credential info
  137. assert self.credentials.service_account_email == "service-account@example.com"
  138. assert self.credentials._scopes == scopes
  139. # Check that the credentials are valid (have a token and are not
  140. # expired)
  141. assert self.credentials.valid
  142. kwargs = get.call_args[1]
  143. assert kwargs["params"] == {"scopes": "three,four"}
  144. assert kwargs["headers"] == {
  145. "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE
  146. }
  147. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  148. def test_refresh_error(self, get):
  149. get.side_effect = exceptions.TransportError("http error")
  150. with pytest.raises(exceptions.RefreshError) as excinfo:
  151. self.credentials.refresh(None)
  152. assert excinfo.match(r"http error")
  153. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  154. def test_before_request_refreshes(self, get):
  155. get.side_effect = [
  156. {
  157. # First request is for sevice account info.
  158. "email": "service-account@example.com",
  159. "scopes": "one two",
  160. },
  161. {
  162. # Second request is for the token.
  163. "access_token": "token",
  164. "expires_in": 500,
  165. },
  166. ]
  167. # Credentials should start as invalid
  168. assert not self.credentials.valid
  169. # before_request should cause a refresh
  170. request = mock.create_autospec(transport.Request, instance=True)
  171. self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
  172. # The refresh endpoint should've been called.
  173. assert get.called
  174. # Credentials should now be valid.
  175. assert self.credentials.valid
  176. def test_with_quota_project(self):
  177. creds = self.credentials_with_all_fields.with_quota_project("project-foo")
  178. assert creds._quota_project_id == "project-foo"
  179. assert creds._service_account_email == FAKE_SERVICE_ACCOUNT_EMAIL
  180. assert creds._scopes == FAKE_SCOPES
  181. assert creds._default_scopes == FAKE_DEFAULT_SCOPES
  182. assert creds.universe_domain == FAKE_UNIVERSE_DOMAIN
  183. assert creds._universe_domain_cached
  184. def test_with_scopes(self):
  185. scopes = ["one", "two"]
  186. creds = self.credentials_with_all_fields.with_scopes(scopes)
  187. assert creds._scopes == scopes
  188. assert creds._quota_project_id == FAKE_QUOTA_PROJECT_ID
  189. assert creds._service_account_email == FAKE_SERVICE_ACCOUNT_EMAIL
  190. assert creds._default_scopes is None
  191. assert creds.universe_domain == FAKE_UNIVERSE_DOMAIN
  192. assert creds._universe_domain_cached
  193. def test_with_universe_domain(self):
  194. creds = self.credentials_with_all_fields.with_universe_domain("universe_domain")
  195. assert creds._scopes == FAKE_SCOPES
  196. assert creds._quota_project_id == FAKE_QUOTA_PROJECT_ID
  197. assert creds._service_account_email == FAKE_SERVICE_ACCOUNT_EMAIL
  198. assert creds._default_scopes == FAKE_DEFAULT_SCOPES
  199. assert creds.universe_domain == "universe_domain"
  200. assert creds._universe_domain_cached
  201. def test_token_usage_metrics(self):
  202. self.credentials.token = "token"
  203. self.credentials.expiry = None
  204. headers = {}
  205. self.credentials.before_request(mock.Mock(), None, None, headers)
  206. assert headers["authorization"] == "Bearer token"
  207. assert headers["x-goog-api-client"] == "cred-type/mds"
  208. @mock.patch(
  209. "google.auth.compute_engine._metadata.get_universe_domain",
  210. return_value="fake_universe_domain",
  211. )
  212. def test_universe_domain(self, get_universe_domain):
  213. # Check the default state
  214. assert not self.credentials._universe_domain_cached
  215. assert self.credentials._universe_domain == "googleapis.com"
  216. # calling the universe_domain property should trigger a call to
  217. # get_universe_domain to fetch the value. The value should be cached.
  218. assert self.credentials.universe_domain == "fake_universe_domain"
  219. assert self.credentials._universe_domain == "fake_universe_domain"
  220. assert self.credentials._universe_domain_cached
  221. get_universe_domain.assert_called_once()
  222. # calling the universe_domain property the second time should use the
  223. # cached value instead of calling get_universe_domain
  224. assert self.credentials.universe_domain == "fake_universe_domain"
  225. get_universe_domain.assert_called_once()
  226. @mock.patch("google.auth.compute_engine._metadata.get_universe_domain")
  227. def test_user_provided_universe_domain(self, get_universe_domain):
  228. assert self.credentials_with_all_fields.universe_domain == FAKE_UNIVERSE_DOMAIN
  229. assert self.credentials_with_all_fields._universe_domain_cached
  230. # Since user provided universe_domain, we will not call the universe
  231. # domain endpoint.
  232. get_universe_domain.assert_not_called()
  233. class TestIDTokenCredentials(object):
  234. credentials = None
  235. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  236. def test_default_state(self, get):
  237. get.side_effect = [
  238. {"email": "service-account@example.com", "scope": ["one", "two"]}
  239. ]
  240. request = mock.create_autospec(transport.Request, instance=True)
  241. self.credentials = credentials.IDTokenCredentials(
  242. request=request, target_audience="https://example.com"
  243. )
  244. assert not self.credentials.valid
  245. # Expiration hasn't been set yet
  246. assert not self.credentials.expired
  247. # Service account email hasn't been populated
  248. assert self.credentials.service_account_email == "service-account@example.com"
  249. # Signer is initialized
  250. assert self.credentials.signer
  251. assert self.credentials.signer_email == "service-account@example.com"
  252. # No quota project
  253. assert not self.credentials._quota_project_id
  254. @mock.patch(
  255. "google.auth._helpers.utcnow",
  256. return_value=datetime.datetime.utcfromtimestamp(0),
  257. )
  258. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  259. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  260. def test_make_authorization_grant_assertion(self, sign, get, utcnow):
  261. get.side_effect = [
  262. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  263. ]
  264. sign.side_effect = [b"signature"]
  265. request = mock.create_autospec(transport.Request, instance=True)
  266. self.credentials = credentials.IDTokenCredentials(
  267. request=request, target_audience="https://audience.com"
  268. )
  269. # Generate authorization grant:
  270. token = self.credentials._make_authorization_grant_assertion()
  271. payload = jwt.decode(token, verify=False)
  272. # The JWT token signature is 'signature' encoded in base 64:
  273. assert token.endswith(b".c2lnbmF0dXJl")
  274. # Check that the credentials have the token and proper expiration
  275. assert payload == {
  276. "aud": "https://www.googleapis.com/oauth2/v4/token",
  277. "exp": 3600,
  278. "iat": 0,
  279. "iss": "service-account@example.com",
  280. "target_audience": "https://audience.com",
  281. }
  282. @mock.patch(
  283. "google.auth._helpers.utcnow",
  284. return_value=datetime.datetime.utcfromtimestamp(0),
  285. )
  286. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  287. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  288. def test_with_service_account(self, sign, get, utcnow):
  289. sign.side_effect = [b"signature"]
  290. request = mock.create_autospec(transport.Request, instance=True)
  291. self.credentials = credentials.IDTokenCredentials(
  292. request=request,
  293. target_audience="https://audience.com",
  294. service_account_email="service-account@other.com",
  295. )
  296. # Generate authorization grant:
  297. token = self.credentials._make_authorization_grant_assertion()
  298. payload = jwt.decode(token, verify=False)
  299. # The JWT token signature is 'signature' encoded in base 64:
  300. assert token.endswith(b".c2lnbmF0dXJl")
  301. # Check that the credentials have the token and proper expiration
  302. assert payload == {
  303. "aud": "https://www.googleapis.com/oauth2/v4/token",
  304. "exp": 3600,
  305. "iat": 0,
  306. "iss": "service-account@other.com",
  307. "target_audience": "https://audience.com",
  308. }
  309. @mock.patch(
  310. "google.auth._helpers.utcnow",
  311. return_value=datetime.datetime.utcfromtimestamp(0),
  312. )
  313. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  314. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  315. def test_additional_claims(self, sign, get, utcnow):
  316. get.side_effect = [
  317. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  318. ]
  319. sign.side_effect = [b"signature"]
  320. request = mock.create_autospec(transport.Request, instance=True)
  321. self.credentials = credentials.IDTokenCredentials(
  322. request=request,
  323. target_audience="https://audience.com",
  324. additional_claims={"foo": "bar"},
  325. )
  326. # Generate authorization grant:
  327. token = self.credentials._make_authorization_grant_assertion()
  328. payload = jwt.decode(token, verify=False)
  329. # The JWT token signature is 'signature' encoded in base 64:
  330. assert token.endswith(b".c2lnbmF0dXJl")
  331. # Check that the credentials have the token and proper expiration
  332. assert payload == {
  333. "aud": "https://www.googleapis.com/oauth2/v4/token",
  334. "exp": 3600,
  335. "iat": 0,
  336. "iss": "service-account@example.com",
  337. "target_audience": "https://audience.com",
  338. "foo": "bar",
  339. }
  340. def test_token_uri(self):
  341. request = mock.create_autospec(transport.Request, instance=True)
  342. self.credentials = credentials.IDTokenCredentials(
  343. request=request,
  344. signer=mock.Mock(),
  345. service_account_email="foo@example.com",
  346. target_audience="https://audience.com",
  347. )
  348. assert self.credentials._token_uri == credentials._DEFAULT_TOKEN_URI
  349. self.credentials = credentials.IDTokenCredentials(
  350. request=request,
  351. signer=mock.Mock(),
  352. service_account_email="foo@example.com",
  353. target_audience="https://audience.com",
  354. token_uri="https://example.com/token",
  355. )
  356. assert self.credentials._token_uri == "https://example.com/token"
  357. @mock.patch(
  358. "google.auth._helpers.utcnow",
  359. return_value=datetime.datetime.utcfromtimestamp(0),
  360. )
  361. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  362. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  363. def test_with_target_audience(self, sign, get, utcnow):
  364. get.side_effect = [
  365. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  366. ]
  367. sign.side_effect = [b"signature"]
  368. request = mock.create_autospec(transport.Request, instance=True)
  369. self.credentials = credentials.IDTokenCredentials(
  370. request=request, target_audience="https://audience.com"
  371. )
  372. self.credentials = self.credentials.with_target_audience("https://actually.not")
  373. # Generate authorization grant:
  374. token = self.credentials._make_authorization_grant_assertion()
  375. payload = jwt.decode(token, verify=False)
  376. # The JWT token signature is 'signature' encoded in base 64:
  377. assert token.endswith(b".c2lnbmF0dXJl")
  378. # Check that the credentials have the token and proper expiration
  379. assert payload == {
  380. "aud": "https://www.googleapis.com/oauth2/v4/token",
  381. "exp": 3600,
  382. "iat": 0,
  383. "iss": "service-account@example.com",
  384. "target_audience": "https://actually.not",
  385. }
  386. # Check that the signer have been initialized with a Request object
  387. assert isinstance(self.credentials._signer._request, transport.Request)
  388. @responses.activate
  389. def test_with_target_audience_integration(self):
  390. """ Test that it is possible to refresh credentials
  391. generated from `with_target_audience`.
  392. Instead of mocking the methods, the HTTP responses
  393. have been mocked.
  394. """
  395. # mock information about credentials
  396. responses.add(
  397. responses.GET,
  398. "http://metadata.google.internal/computeMetadata/v1/instance/"
  399. "service-accounts/default/?recursive=true",
  400. status=200,
  401. content_type="application/json",
  402. json={
  403. "scopes": "email",
  404. "email": "service-account@example.com",
  405. "aliases": ["default"],
  406. },
  407. )
  408. # mock token for credentials
  409. responses.add(
  410. responses.GET,
  411. "http://metadata.google.internal/computeMetadata/v1/instance/"
  412. "service-accounts/service-account@example.com/token",
  413. status=200,
  414. content_type="application/json",
  415. json={
  416. "access_token": "some-token",
  417. "expires_in": 3210,
  418. "token_type": "Bearer",
  419. },
  420. )
  421. # mock sign blob endpoint
  422. signature = base64.b64encode(b"some-signature").decode("utf-8")
  423. responses.add(
  424. responses.POST,
  425. "https://iamcredentials.googleapis.com/v1/projects/-/"
  426. "serviceAccounts/service-account@example.com:signBlob",
  427. status=200,
  428. content_type="application/json",
  429. json={"keyId": "some-key-id", "signedBlob": signature},
  430. )
  431. id_token = "{}.{}.{}".format(
  432. base64.b64encode(b'{"some":"some"}').decode("utf-8"),
  433. base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
  434. base64.b64encode(b"token").decode("utf-8"),
  435. )
  436. # mock id token endpoint
  437. responses.add(
  438. responses.POST,
  439. "https://www.googleapis.com/oauth2/v4/token",
  440. status=200,
  441. content_type="application/json",
  442. json={"id_token": id_token, "expiry": 3210},
  443. )
  444. self.credentials = credentials.IDTokenCredentials(
  445. request=requests.Request(),
  446. service_account_email="service-account@example.com",
  447. target_audience="https://audience.com",
  448. )
  449. self.credentials = self.credentials.with_target_audience("https://actually.not")
  450. self.credentials.refresh(requests.Request())
  451. assert self.credentials.token is not None
  452. @mock.patch(
  453. "google.auth._helpers.utcnow",
  454. return_value=datetime.datetime.utcfromtimestamp(0),
  455. )
  456. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  457. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  458. def test_with_quota_project(self, sign, get, utcnow):
  459. get.side_effect = [
  460. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  461. ]
  462. sign.side_effect = [b"signature"]
  463. request = mock.create_autospec(transport.Request, instance=True)
  464. self.credentials = credentials.IDTokenCredentials(
  465. request=request, target_audience="https://audience.com"
  466. )
  467. self.credentials = self.credentials.with_quota_project("project-foo")
  468. assert self.credentials._quota_project_id == "project-foo"
  469. # Generate authorization grant:
  470. token = self.credentials._make_authorization_grant_assertion()
  471. payload = jwt.decode(token, verify=False)
  472. # The JWT token signature is 'signature' encoded in base 64:
  473. assert token.endswith(b".c2lnbmF0dXJl")
  474. # Check that the credentials have the token and proper expiration
  475. assert payload == {
  476. "aud": "https://www.googleapis.com/oauth2/v4/token",
  477. "exp": 3600,
  478. "iat": 0,
  479. "iss": "service-account@example.com",
  480. "target_audience": "https://audience.com",
  481. }
  482. # Check that the signer have been initialized with a Request object
  483. assert isinstance(self.credentials._signer._request, transport.Request)
  484. @mock.patch(
  485. "google.auth._helpers.utcnow",
  486. return_value=datetime.datetime.utcfromtimestamp(0),
  487. )
  488. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  489. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  490. def test_with_token_uri(self, sign, get, utcnow):
  491. get.side_effect = [
  492. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  493. ]
  494. sign.side_effect = [b"signature"]
  495. request = mock.create_autospec(transport.Request, instance=True)
  496. self.credentials = credentials.IDTokenCredentials(
  497. request=request,
  498. target_audience="https://audience.com",
  499. token_uri="http://xyz.com",
  500. )
  501. assert self.credentials._token_uri == "http://xyz.com"
  502. creds_with_token_uri = self.credentials.with_token_uri("http://example.com")
  503. assert creds_with_token_uri._token_uri == "http://example.com"
  504. @mock.patch(
  505. "google.auth._helpers.utcnow",
  506. return_value=datetime.datetime.utcfromtimestamp(0),
  507. )
  508. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  509. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  510. def test_with_token_uri_exception(self, sign, get, utcnow):
  511. get.side_effect = [
  512. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  513. ]
  514. sign.side_effect = [b"signature"]
  515. request = mock.create_autospec(transport.Request, instance=True)
  516. self.credentials = credentials.IDTokenCredentials(
  517. request=request,
  518. target_audience="https://audience.com",
  519. use_metadata_identity_endpoint=True,
  520. )
  521. assert self.credentials._token_uri is None
  522. with pytest.raises(ValueError):
  523. self.credentials.with_token_uri("http://example.com")
  524. @responses.activate
  525. def test_with_quota_project_integration(self):
  526. """ Test that it is possible to refresh credentials
  527. generated from `with_quota_project`.
  528. Instead of mocking the methods, the HTTP responses
  529. have been mocked.
  530. """
  531. # mock information about credentials
  532. responses.add(
  533. responses.GET,
  534. "http://metadata.google.internal/computeMetadata/v1/instance/"
  535. "service-accounts/default/?recursive=true",
  536. status=200,
  537. content_type="application/json",
  538. json={
  539. "scopes": "email",
  540. "email": "service-account@example.com",
  541. "aliases": ["default"],
  542. },
  543. )
  544. # mock token for credentials
  545. responses.add(
  546. responses.GET,
  547. "http://metadata.google.internal/computeMetadata/v1/instance/"
  548. "service-accounts/service-account@example.com/token",
  549. status=200,
  550. content_type="application/json",
  551. json={
  552. "access_token": "some-token",
  553. "expires_in": 3210,
  554. "token_type": "Bearer",
  555. },
  556. )
  557. # mock sign blob endpoint
  558. signature = base64.b64encode(b"some-signature").decode("utf-8")
  559. responses.add(
  560. responses.POST,
  561. "https://iamcredentials.googleapis.com/v1/projects/-/"
  562. "serviceAccounts/service-account@example.com:signBlob",
  563. status=200,
  564. content_type="application/json",
  565. json={"keyId": "some-key-id", "signedBlob": signature},
  566. )
  567. id_token = "{}.{}.{}".format(
  568. base64.b64encode(b'{"some":"some"}').decode("utf-8"),
  569. base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
  570. base64.b64encode(b"token").decode("utf-8"),
  571. )
  572. # mock id token endpoint
  573. responses.add(
  574. responses.POST,
  575. "https://www.googleapis.com/oauth2/v4/token",
  576. status=200,
  577. content_type="application/json",
  578. json={"id_token": id_token, "expiry": 3210},
  579. )
  580. self.credentials = credentials.IDTokenCredentials(
  581. request=requests.Request(),
  582. service_account_email="service-account@example.com",
  583. target_audience="https://audience.com",
  584. )
  585. self.credentials = self.credentials.with_quota_project("project-foo")
  586. self.credentials.refresh(requests.Request())
  587. assert self.credentials.token is not None
  588. assert self.credentials._quota_project_id == "project-foo"
  589. @mock.patch(
  590. "google.auth._helpers.utcnow",
  591. return_value=datetime.datetime.utcfromtimestamp(0),
  592. )
  593. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  594. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  595. @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
  596. def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
  597. get.side_effect = [
  598. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  599. ]
  600. sign.side_effect = [b"signature"]
  601. id_token_jwt_grant.side_effect = [
  602. ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
  603. ]
  604. request = mock.create_autospec(transport.Request, instance=True)
  605. self.credentials = credentials.IDTokenCredentials(
  606. request=request, target_audience="https://audience.com"
  607. )
  608. # Refresh credentials
  609. self.credentials.refresh(None)
  610. # Check that the credentials have the token and proper expiration
  611. assert self.credentials.token == "idtoken"
  612. assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600))
  613. # Check the credential info
  614. assert self.credentials.service_account_email == "service-account@example.com"
  615. # Check that the credentials are valid (have a token and are not
  616. # expired)
  617. assert self.credentials.valid
  618. @mock.patch(
  619. "google.auth._helpers.utcnow",
  620. return_value=datetime.datetime.utcfromtimestamp(0),
  621. )
  622. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  623. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  624. def test_refresh_error(self, sign, get, utcnow):
  625. get.side_effect = [
  626. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  627. ]
  628. sign.side_effect = [b"signature"]
  629. request = mock.create_autospec(transport.Request, instance=True)
  630. response = mock.Mock()
  631. response.data = b'{"error": "http error"}'
  632. response.status = 404 # Throw a 404 so the request is not retried.
  633. request.side_effect = [response]
  634. self.credentials = credentials.IDTokenCredentials(
  635. request=request, target_audience="https://audience.com"
  636. )
  637. with pytest.raises(exceptions.RefreshError) as excinfo:
  638. self.credentials.refresh(request)
  639. assert excinfo.match(r"http error")
  640. @mock.patch(
  641. "google.auth._helpers.utcnow",
  642. return_value=datetime.datetime.utcfromtimestamp(0),
  643. )
  644. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  645. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  646. @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
  647. def test_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow):
  648. get.side_effect = [
  649. {"email": "service-account@example.com", "scopes": "one two"}
  650. ]
  651. sign.side_effect = [b"signature"]
  652. id_token_jwt_grant.side_effect = [
  653. ("idtoken", datetime.datetime.utcfromtimestamp(3600), {})
  654. ]
  655. request = mock.create_autospec(transport.Request, instance=True)
  656. self.credentials = credentials.IDTokenCredentials(
  657. request=request, target_audience="https://audience.com"
  658. )
  659. # Credentials should start as invalid
  660. assert not self.credentials.valid
  661. # before_request should cause a refresh
  662. request = mock.create_autospec(transport.Request, instance=True)
  663. self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {})
  664. # The refresh endpoint should've been called.
  665. assert get.called
  666. # Credentials should now be valid.
  667. assert self.credentials.valid
  668. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  669. @mock.patch("google.auth.iam.Signer.sign", autospec=True)
  670. def test_sign_bytes(self, sign, get):
  671. get.side_effect = [
  672. {"email": "service-account@example.com", "scopes": ["one", "two"]}
  673. ]
  674. sign.side_effect = [b"signature"]
  675. request = mock.create_autospec(transport.Request, instance=True)
  676. response = mock.Mock()
  677. response.data = b'{"signature": "c2lnbmF0dXJl"}'
  678. response.status = 200
  679. request.side_effect = [response]
  680. self.credentials = credentials.IDTokenCredentials(
  681. request=request, target_audience="https://audience.com"
  682. )
  683. # Generate authorization grant:
  684. signature = self.credentials.sign_bytes(b"some bytes")
  685. # The JWT token signature is 'signature' encoded in base 64:
  686. assert signature == b"signature"
  687. @mock.patch(
  688. "google.auth.metrics.token_request_id_token_mds",
  689. return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
  690. )
  691. @mock.patch(
  692. "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
  693. )
  694. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  695. def test_get_id_token_from_metadata(
  696. self, get, get_service_account_info, mock_metrics_header_value
  697. ):
  698. get.return_value = SAMPLE_ID_TOKEN
  699. get_service_account_info.return_value = {"email": "foo@example.com"}
  700. cred = credentials.IDTokenCredentials(
  701. mock.Mock(), "audience", use_metadata_identity_endpoint=True
  702. )
  703. cred.refresh(request=mock.Mock())
  704. assert get.call_args.kwargs["headers"] == {
  705. "x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
  706. }
  707. assert cred.token == SAMPLE_ID_TOKEN
  708. assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP)
  709. assert cred._use_metadata_identity_endpoint
  710. assert cred._signer is None
  711. assert cred._token_uri is None
  712. assert cred._service_account_email == "foo@example.com"
  713. assert cred._target_audience == "audience"
  714. with pytest.raises(ValueError):
  715. cred.sign_bytes(b"bytes")
  716. @mock.patch(
  717. "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
  718. )
  719. def test_with_target_audience_for_metadata(self, get_service_account_info):
  720. get_service_account_info.return_value = {"email": "foo@example.com"}
  721. cred = credentials.IDTokenCredentials(
  722. mock.Mock(), "audience", use_metadata_identity_endpoint=True
  723. )
  724. cred = cred.with_target_audience("new_audience")
  725. assert cred._target_audience == "new_audience"
  726. assert cred._use_metadata_identity_endpoint
  727. assert cred._signer is None
  728. assert cred._token_uri is None
  729. assert cred._service_account_email == "foo@example.com"
  730. @mock.patch(
  731. "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
  732. )
  733. def test_id_token_with_quota_project(self, get_service_account_info):
  734. get_service_account_info.return_value = {"email": "foo@example.com"}
  735. cred = credentials.IDTokenCredentials(
  736. mock.Mock(), "audience", use_metadata_identity_endpoint=True
  737. )
  738. cred = cred.with_quota_project("project-foo")
  739. assert cred._quota_project_id == "project-foo"
  740. assert cred._use_metadata_identity_endpoint
  741. assert cred._signer is None
  742. assert cred._token_uri is None
  743. assert cred._service_account_email == "foo@example.com"
  744. @mock.patch(
  745. "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
  746. )
  747. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  748. def test_invalid_id_token_from_metadata(self, get, get_service_account_info):
  749. get.return_value = "invalid_id_token"
  750. get_service_account_info.return_value = {"email": "foo@example.com"}
  751. cred = credentials.IDTokenCredentials(
  752. mock.Mock(), "audience", use_metadata_identity_endpoint=True
  753. )
  754. with pytest.raises(ValueError):
  755. cred.refresh(request=mock.Mock())
  756. @mock.patch(
  757. "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
  758. )
  759. @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
  760. def test_transport_error_from_metadata(self, get, get_service_account_info):
  761. get.side_effect = exceptions.TransportError("transport error")
  762. get_service_account_info.return_value = {"email": "foo@example.com"}
  763. cred = credentials.IDTokenCredentials(
  764. mock.Mock(), "audience", use_metadata_identity_endpoint=True
  765. )
  766. with pytest.raises(exceptions.RefreshError) as excinfo:
  767. cred.refresh(request=mock.Mock())
  768. assert excinfo.match(r"transport error")
  769. def test_get_id_token_from_metadata_constructor(self):
  770. with pytest.raises(ValueError):
  771. credentials.IDTokenCredentials(
  772. mock.Mock(),
  773. "audience",
  774. use_metadata_identity_endpoint=True,
  775. token_uri="token_uri",
  776. )
  777. with pytest.raises(ValueError):
  778. credentials.IDTokenCredentials(
  779. mock.Mock(),
  780. "audience",
  781. use_metadata_identity_endpoint=True,
  782. signer=mock.Mock(),
  783. )
  784. with pytest.raises(ValueError):
  785. credentials.IDTokenCredentials(
  786. mock.Mock(),
  787. "audience",
  788. use_metadata_identity_endpoint=True,
  789. additional_claims={"key", "value"},
  790. )
  791. with pytest.raises(ValueError):
  792. credentials.IDTokenCredentials(
  793. mock.Mock(),
  794. "audience",
  795. use_metadata_identity_endpoint=True,
  796. service_account_email="foo@example.com",
  797. )