test_reauth.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. # Copyright 2021 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 copy
  15. import mock
  16. import pytest # type: ignore
  17. from google.auth import exceptions
  18. from google.oauth2 import reauth
  19. MOCK_REQUEST = mock.Mock()
  20. CHALLENGES_RESPONSE_TEMPLATE = {
  21. "status": "CHALLENGE_REQUIRED",
  22. "sessionId": "123",
  23. "challenges": [
  24. {
  25. "status": "READY",
  26. "challengeId": 1,
  27. "challengeType": "PASSWORD",
  28. "securityKey": {},
  29. }
  30. ],
  31. }
  32. CHALLENGES_RESPONSE_AUTHENTICATED = {
  33. "status": "AUTHENTICATED",
  34. "sessionId": "123",
  35. "encodedProofOfReauthToken": "new_rapt_token",
  36. }
  37. REAUTH_START_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/re-start"
  38. REAUTH_CONTINUE_METRICS_HEADER_VALUE = (
  39. "gl-python/3.7 auth/1.1 auth-request-type/re-cont"
  40. )
  41. TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 cred-type/u"
  42. class MockChallenge(object):
  43. def __init__(self, name, locally_eligible, challenge_input):
  44. self.name = name
  45. self.is_locally_eligible = locally_eligible
  46. self.challenge_input = challenge_input
  47. def obtain_challenge_input(self, metadata):
  48. return self.challenge_input
  49. def _test_is_interactive():
  50. with mock.patch("sys.stdin.isatty", return_value=True):
  51. assert reauth.is_interactive()
  52. @mock.patch(
  53. "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
  54. )
  55. def test__get_challenges(mock_metrics_header_value):
  56. with mock.patch(
  57. "google.oauth2._client._token_endpoint_request"
  58. ) as mock_token_endpoint_request:
  59. reauth._get_challenges(MOCK_REQUEST, ["SAML"], "token")
  60. mock_token_endpoint_request.assert_called_with(
  61. MOCK_REQUEST,
  62. reauth._REAUTH_API + ":start",
  63. {"supportedChallengeTypes": ["SAML"]},
  64. access_token="token",
  65. use_json=True,
  66. headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
  67. )
  68. @mock.patch(
  69. "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
  70. )
  71. def test__get_challenges_with_scopes(mock_metrics_header_value):
  72. with mock.patch(
  73. "google.oauth2._client._token_endpoint_request"
  74. ) as mock_token_endpoint_request:
  75. reauth._get_challenges(
  76. MOCK_REQUEST, ["SAML"], "token", requested_scopes=["scope"]
  77. )
  78. mock_token_endpoint_request.assert_called_with(
  79. MOCK_REQUEST,
  80. reauth._REAUTH_API + ":start",
  81. {
  82. "supportedChallengeTypes": ["SAML"],
  83. "oauthScopesForDomainPolicyLookup": ["scope"],
  84. },
  85. access_token="token",
  86. use_json=True,
  87. headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
  88. )
  89. @mock.patch(
  90. "google.auth.metrics.reauth_continue",
  91. return_value=REAUTH_CONTINUE_METRICS_HEADER_VALUE,
  92. )
  93. def test__send_challenge_result(mock_metrics_header_value):
  94. with mock.patch(
  95. "google.oauth2._client._token_endpoint_request"
  96. ) as mock_token_endpoint_request:
  97. reauth._send_challenge_result(
  98. MOCK_REQUEST, "123", "1", {"credential": "password"}, "token"
  99. )
  100. mock_token_endpoint_request.assert_called_with(
  101. MOCK_REQUEST,
  102. reauth._REAUTH_API + "/123:continue",
  103. {
  104. "sessionId": "123",
  105. "challengeId": "1",
  106. "action": "RESPOND",
  107. "proposalResponse": {"credential": "password"},
  108. },
  109. access_token="token",
  110. use_json=True,
  111. headers={"x-goog-api-client": REAUTH_CONTINUE_METRICS_HEADER_VALUE},
  112. )
  113. def test__run_next_challenge_not_ready():
  114. challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
  115. challenges_response["challenges"][0]["status"] = "STATUS_UNSPECIFIED"
  116. assert (
  117. reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token") is None
  118. )
  119. def test__run_next_challenge_not_supported():
  120. challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
  121. challenges_response["challenges"][0]["challengeType"] = "CHALLENGE_TYPE_UNSPECIFIED"
  122. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  123. reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token")
  124. assert excinfo.match(r"Unsupported challenge type CHALLENGE_TYPE_UNSPECIFIED")
  125. def test__run_next_challenge_not_locally_eligible():
  126. mock_challenge = MockChallenge("PASSWORD", False, "challenge_input")
  127. with mock.patch(
  128. "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
  129. ):
  130. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  131. reauth._run_next_challenge(
  132. CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
  133. )
  134. assert excinfo.match(r"Challenge PASSWORD is not locally eligible")
  135. def test__run_next_challenge_no_challenge_input():
  136. mock_challenge = MockChallenge("PASSWORD", True, None)
  137. with mock.patch(
  138. "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
  139. ):
  140. assert (
  141. reauth._run_next_challenge(
  142. CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
  143. )
  144. is None
  145. )
  146. def test__run_next_challenge_success():
  147. mock_challenge = MockChallenge("PASSWORD", True, {"credential": "password"})
  148. with mock.patch(
  149. "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge}
  150. ):
  151. with mock.patch(
  152. "google.oauth2.reauth._send_challenge_result"
  153. ) as mock_send_challenge_result:
  154. reauth._run_next_challenge(
  155. CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token"
  156. )
  157. mock_send_challenge_result.assert_called_with(
  158. MOCK_REQUEST, "123", 1, {"credential": "password"}, "token"
  159. )
  160. def test__obtain_rapt_authenticated():
  161. with mock.patch(
  162. "google.oauth2.reauth._get_challenges",
  163. return_value=CHALLENGES_RESPONSE_AUTHENTICATED,
  164. ):
  165. assert reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
  166. def test__obtain_rapt_authenticated_after_run_next_challenge():
  167. with mock.patch(
  168. "google.oauth2.reauth._get_challenges",
  169. return_value=CHALLENGES_RESPONSE_TEMPLATE,
  170. ):
  171. with mock.patch(
  172. "google.oauth2.reauth._run_next_challenge",
  173. side_effect=[
  174. CHALLENGES_RESPONSE_TEMPLATE,
  175. CHALLENGES_RESPONSE_AUTHENTICATED,
  176. ],
  177. ):
  178. with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
  179. assert (
  180. reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token"
  181. )
  182. def test__obtain_rapt_unsupported_status():
  183. challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
  184. challenges_response["status"] = "STATUS_UNSPECIFIED"
  185. with mock.patch(
  186. "google.oauth2.reauth._get_challenges", return_value=challenges_response
  187. ):
  188. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  189. reauth._obtain_rapt(MOCK_REQUEST, "token", None)
  190. assert excinfo.match(r"API error: STATUS_UNSPECIFIED")
  191. def test__obtain_rapt_no_challenge_output():
  192. challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE)
  193. with mock.patch(
  194. "google.oauth2.reauth._get_challenges", return_value=challenges_response
  195. ):
  196. with mock.patch("google.oauth2.reauth.is_interactive", return_value=True):
  197. with mock.patch(
  198. "google.oauth2.reauth._run_next_challenge", return_value=None
  199. ):
  200. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  201. reauth._obtain_rapt(MOCK_REQUEST, "token", None)
  202. assert excinfo.match(r"Failed to obtain rapt token")
  203. def test__obtain_rapt_not_interactive():
  204. with mock.patch(
  205. "google.oauth2.reauth._get_challenges",
  206. return_value=CHALLENGES_RESPONSE_TEMPLATE,
  207. ):
  208. with mock.patch("google.oauth2.reauth.is_interactive", return_value=False):
  209. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  210. reauth._obtain_rapt(MOCK_REQUEST, "token", None)
  211. assert excinfo.match(r"not in an interactive session")
  212. def test__obtain_rapt_not_authenticated():
  213. with mock.patch(
  214. "google.oauth2.reauth._get_challenges",
  215. return_value=CHALLENGES_RESPONSE_TEMPLATE,
  216. ):
  217. with mock.patch("google.oauth2.reauth.RUN_CHALLENGE_RETRY_LIMIT", 0):
  218. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  219. reauth._obtain_rapt(MOCK_REQUEST, "token", None)
  220. assert excinfo.match(r"Reauthentication failed")
  221. def test_get_rapt_token():
  222. with mock.patch(
  223. "google.oauth2._client.refresh_grant", return_value=("token", None, None, None)
  224. ) as mock_refresh_grant:
  225. with mock.patch(
  226. "google.oauth2.reauth._obtain_rapt", return_value="new_rapt_token"
  227. ) as mock_obtain_rapt:
  228. assert (
  229. reauth.get_rapt_token(
  230. MOCK_REQUEST,
  231. "client_id",
  232. "client_secret",
  233. "refresh_token",
  234. "token_uri",
  235. )
  236. == "new_rapt_token"
  237. )
  238. mock_refresh_grant.assert_called_with(
  239. request=MOCK_REQUEST,
  240. client_id="client_id",
  241. client_secret="client_secret",
  242. refresh_token="refresh_token",
  243. token_uri="token_uri",
  244. scopes=[reauth._REAUTH_SCOPE],
  245. )
  246. mock_obtain_rapt.assert_called_with(
  247. MOCK_REQUEST, "token", requested_scopes=None
  248. )
  249. @mock.patch(
  250. "google.auth.metrics.token_request_user",
  251. return_value=TOKEN_REQUEST_METRICS_HEADER_VALUE,
  252. )
  253. def test_refresh_grant_failed(mock_metrics_header_value):
  254. with mock.patch(
  255. "google.oauth2._client._token_endpoint_request_no_throw"
  256. ) as mock_token_request:
  257. mock_token_request.return_value = (False, {"error": "Bad request"}, False)
  258. with pytest.raises(exceptions.RefreshError) as excinfo:
  259. reauth.refresh_grant(
  260. MOCK_REQUEST,
  261. "token_uri",
  262. "refresh_token",
  263. "client_id",
  264. "client_secret",
  265. scopes=["foo", "bar"],
  266. rapt_token="rapt_token",
  267. enable_reauth_refresh=True,
  268. )
  269. assert excinfo.match(r"Bad request")
  270. assert not excinfo.value.retryable
  271. mock_token_request.assert_called_with(
  272. MOCK_REQUEST,
  273. "token_uri",
  274. {
  275. "grant_type": "refresh_token",
  276. "client_id": "client_id",
  277. "client_secret": "client_secret",
  278. "refresh_token": "refresh_token",
  279. "scope": "foo bar",
  280. "rapt": "rapt_token",
  281. },
  282. headers={"x-goog-api-client": TOKEN_REQUEST_METRICS_HEADER_VALUE},
  283. )
  284. def test_refresh_grant_failed_with_string_type_response():
  285. with mock.patch(
  286. "google.oauth2._client._token_endpoint_request_no_throw"
  287. ) as mock_token_request:
  288. mock_token_request.return_value = (False, "string type error", False)
  289. with pytest.raises(exceptions.RefreshError) as excinfo:
  290. reauth.refresh_grant(
  291. MOCK_REQUEST,
  292. "token_uri",
  293. "refresh_token",
  294. "client_id",
  295. "client_secret",
  296. scopes=["foo", "bar"],
  297. rapt_token="rapt_token",
  298. enable_reauth_refresh=True,
  299. )
  300. assert excinfo.match(r"string type error")
  301. assert not excinfo.value.retryable
  302. def test_refresh_grant_success():
  303. with mock.patch(
  304. "google.oauth2._client._token_endpoint_request_no_throw"
  305. ) as mock_token_request:
  306. mock_token_request.side_effect = [
  307. (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True),
  308. (True, {"access_token": "access_token"}, None),
  309. ]
  310. with mock.patch(
  311. "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token"
  312. ):
  313. assert reauth.refresh_grant(
  314. MOCK_REQUEST,
  315. "token_uri",
  316. "refresh_token",
  317. "client_id",
  318. "client_secret",
  319. enable_reauth_refresh=True,
  320. ) == (
  321. "access_token",
  322. "refresh_token",
  323. None,
  324. {"access_token": "access_token"},
  325. "new_rapt_token",
  326. )
  327. def test_refresh_grant_reauth_refresh_disabled():
  328. with mock.patch(
  329. "google.oauth2._client._token_endpoint_request_no_throw"
  330. ) as mock_token_request:
  331. mock_token_request.side_effect = [
  332. (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True),
  333. (True, {"access_token": "access_token"}, None),
  334. ]
  335. with pytest.raises(exceptions.RefreshError) as excinfo:
  336. reauth.refresh_grant(
  337. MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret"
  338. )
  339. assert excinfo.match(r"Reauthentication is needed")