test_challenges.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. """Tests for the reauth module."""
  15. import base64
  16. import sys
  17. import mock
  18. import pytest # type: ignore
  19. import pyu2f # type: ignore
  20. from google.auth import exceptions
  21. from google.oauth2 import challenges
  22. def test_get_user_password():
  23. with mock.patch("getpass.getpass", return_value="foo"):
  24. assert challenges.get_user_password("") == "foo"
  25. def test_security_key():
  26. metadata = {
  27. "status": "READY",
  28. "challengeId": 2,
  29. "challengeType": "SECURITY_KEY",
  30. "securityKey": {
  31. "applicationId": "security_key_application_id",
  32. "challenges": [
  33. {
  34. "keyHandle": "some_key",
  35. "challenge": base64.urlsafe_b64encode(
  36. "some_challenge".encode("ascii")
  37. ).decode("ascii"),
  38. }
  39. ],
  40. "relyingPartyId": "security_key_application_id",
  41. },
  42. }
  43. mock_key = mock.Mock()
  44. challenge = challenges.SecurityKeyChallenge()
  45. # Test the case that security key challenge is passed with applicationId and
  46. # relyingPartyId the same.
  47. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
  48. with mock.patch(
  49. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  50. ) as mock_authenticate:
  51. mock_authenticate.return_value = "security key response"
  52. assert challenge.name == "SECURITY_KEY"
  53. assert challenge.is_locally_eligible
  54. assert challenge.obtain_challenge_input(metadata) == {
  55. "securityKey": "security key response"
  56. }
  57. mock_authenticate.assert_called_with(
  58. "security_key_application_id",
  59. [{"key": mock_key, "challenge": b"some_challenge"}],
  60. print_callback=sys.stderr.write,
  61. )
  62. # Test the case that security key challenge is passed with applicationId and
  63. # relyingPartyId different, first call works.
  64. metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
  65. sys.stderr.write("metadata=" + str(metadata) + "\n")
  66. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
  67. with mock.patch(
  68. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  69. ) as mock_authenticate:
  70. mock_authenticate.return_value = "security key response"
  71. assert challenge.name == "SECURITY_KEY"
  72. assert challenge.is_locally_eligible
  73. assert challenge.obtain_challenge_input(metadata) == {
  74. "securityKey": "security key response"
  75. }
  76. mock_authenticate.assert_called_with(
  77. "security_key_relying_party_id",
  78. [{"key": mock_key, "challenge": b"some_challenge"}],
  79. print_callback=sys.stderr.write,
  80. )
  81. # Test the case that security key challenge is passed with applicationId and
  82. # relyingPartyId different, first call fails, requires retry.
  83. metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
  84. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
  85. with mock.patch(
  86. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  87. ) as mock_authenticate:
  88. assert challenge.name == "SECURITY_KEY"
  89. assert challenge.is_locally_eligible
  90. mock_authenticate.side_effect = [
  91. pyu2f.errors.U2FError(pyu2f.errors.U2FError.DEVICE_INELIGIBLE),
  92. "security key response",
  93. ]
  94. assert challenge.obtain_challenge_input(metadata) == {
  95. "securityKey": "security key response"
  96. }
  97. calls = [
  98. mock.call(
  99. "security_key_relying_party_id",
  100. [{"key": mock_key, "challenge": b"some_challenge"}],
  101. print_callback=sys.stderr.write,
  102. ),
  103. mock.call(
  104. "security_key_application_id",
  105. [{"key": mock_key, "challenge": b"some_challenge"}],
  106. print_callback=sys.stderr.write,
  107. ),
  108. ]
  109. mock_authenticate.assert_has_calls(calls)
  110. # Test various types of exceptions.
  111. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
  112. with mock.patch(
  113. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  114. ) as mock_authenticate:
  115. mock_authenticate.side_effect = pyu2f.errors.U2FError(
  116. pyu2f.errors.U2FError.DEVICE_INELIGIBLE
  117. )
  118. assert challenge.obtain_challenge_input(metadata) is None
  119. with mock.patch(
  120. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  121. ) as mock_authenticate:
  122. mock_authenticate.side_effect = pyu2f.errors.U2FError(
  123. pyu2f.errors.U2FError.TIMEOUT
  124. )
  125. assert challenge.obtain_challenge_input(metadata) is None
  126. with mock.patch(
  127. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  128. ) as mock_authenticate:
  129. mock_authenticate.side_effect = pyu2f.errors.PluginError()
  130. assert challenge.obtain_challenge_input(metadata) is None
  131. with mock.patch(
  132. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  133. ) as mock_authenticate:
  134. mock_authenticate.side_effect = pyu2f.errors.U2FError(
  135. pyu2f.errors.U2FError.BAD_REQUEST
  136. )
  137. with pytest.raises(pyu2f.errors.U2FError):
  138. challenge.obtain_challenge_input(metadata)
  139. with mock.patch(
  140. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  141. ) as mock_authenticate:
  142. mock_authenticate.side_effect = pyu2f.errors.NoDeviceFoundError()
  143. assert challenge.obtain_challenge_input(metadata) is None
  144. with mock.patch(
  145. "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
  146. ) as mock_authenticate:
  147. mock_authenticate.side_effect = pyu2f.errors.UnsupportedVersionException()
  148. with pytest.raises(pyu2f.errors.UnsupportedVersionException):
  149. challenge.obtain_challenge_input(metadata)
  150. with mock.patch.dict("sys.modules"):
  151. sys.modules["pyu2f"] = None
  152. with pytest.raises(exceptions.ReauthFailError) as excinfo:
  153. challenge.obtain_challenge_input(metadata)
  154. assert excinfo.match(r"pyu2f dependency is required")
  155. @mock.patch("getpass.getpass", return_value="foo")
  156. def test_password_challenge(getpass_mock):
  157. challenge = challenges.PasswordChallenge()
  158. with mock.patch("getpass.getpass", return_value="foo"):
  159. assert challenge.is_locally_eligible
  160. assert challenge.name == "PASSWORD"
  161. assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
  162. "credential": "foo"
  163. }
  164. with mock.patch("getpass.getpass", return_value=None):
  165. assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
  166. "credential": " "
  167. }
  168. def test_saml_challenge():
  169. challenge = challenges.SamlChallenge()
  170. assert challenge.is_locally_eligible
  171. assert challenge.name == "SAML"
  172. with pytest.raises(exceptions.ReauthSamlChallengeFailError):
  173. challenge.obtain_challenge_input(None)