test_credentials.py 37 KB

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