challenges.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. """ Challenges for reauthentication.
  15. """
  16. import abc
  17. import base64
  18. import getpass
  19. import sys
  20. import six
  21. from google.auth import _helpers
  22. from google.auth import exceptions
  23. REAUTH_ORIGIN = "https://accounts.google.com"
  24. SAML_CHALLENGE_MESSAGE = (
  25. "Please run `gcloud auth login` to complete reauthentication with SAML."
  26. )
  27. def get_user_password(text):
  28. """Get password from user.
  29. Override this function with a different logic if you are using this library
  30. outside a CLI.
  31. Args:
  32. text (str): message for the password prompt.
  33. Returns:
  34. str: password string.
  35. """
  36. return getpass.getpass(text)
  37. @six.add_metaclass(abc.ABCMeta)
  38. class ReauthChallenge(object):
  39. """Base class for reauth challenges."""
  40. @property
  41. @abc.abstractmethod
  42. def name(self): # pragma: NO COVER
  43. """Returns the name of the challenge."""
  44. raise NotImplementedError("name property must be implemented")
  45. @property
  46. @abc.abstractmethod
  47. def is_locally_eligible(self): # pragma: NO COVER
  48. """Returns true if a challenge is supported locally on this machine."""
  49. raise NotImplementedError("is_locally_eligible property must be implemented")
  50. @abc.abstractmethod
  51. def obtain_challenge_input(self, metadata): # pragma: NO COVER
  52. """Performs logic required to obtain credentials and returns it.
  53. Args:
  54. metadata (Mapping): challenge metadata returned in the 'challenges' field in
  55. the initial reauth request. Includes the 'challengeType' field
  56. and other challenge-specific fields.
  57. Returns:
  58. response that will be send to the reauth service as the content of
  59. the 'proposalResponse' field in the request body. Usually a dict
  60. with the keys specific to the challenge. For example,
  61. ``{'credential': password}`` for password challenge.
  62. """
  63. raise NotImplementedError("obtain_challenge_input method must be implemented")
  64. class PasswordChallenge(ReauthChallenge):
  65. """Challenge that asks for user's password."""
  66. @property
  67. def name(self):
  68. return "PASSWORD"
  69. @property
  70. def is_locally_eligible(self):
  71. return True
  72. @_helpers.copy_docstring(ReauthChallenge)
  73. def obtain_challenge_input(self, unused_metadata):
  74. passwd = get_user_password("Please enter your password:")
  75. if not passwd:
  76. passwd = " " # avoid the server crashing in case of no password :D
  77. return {"credential": passwd}
  78. class SecurityKeyChallenge(ReauthChallenge):
  79. """Challenge that asks for user's security key touch."""
  80. @property
  81. def name(self):
  82. return "SECURITY_KEY"
  83. @property
  84. def is_locally_eligible(self):
  85. return True
  86. @_helpers.copy_docstring(ReauthChallenge)
  87. def obtain_challenge_input(self, metadata):
  88. try:
  89. import pyu2f.convenience.authenticator # type: ignore
  90. import pyu2f.errors # type: ignore
  91. import pyu2f.model # type: ignore
  92. except ImportError:
  93. raise exceptions.ReauthFailError(
  94. "pyu2f dependency is required to use Security key reauth feature. "
  95. "It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
  96. )
  97. sk = metadata["securityKey"]
  98. challenges = sk["challenges"]
  99. app_id = sk["applicationId"]
  100. challenge_data = []
  101. for c in challenges:
  102. kh = c["keyHandle"].encode("ascii")
  103. key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
  104. challenge = c["challenge"].encode("ascii")
  105. challenge = base64.urlsafe_b64decode(challenge)
  106. challenge_data.append({"key": key, "challenge": challenge})
  107. try:
  108. api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
  109. REAUTH_ORIGIN
  110. )
  111. response = api.Authenticate(
  112. app_id, challenge_data, print_callback=sys.stderr.write
  113. )
  114. return {"securityKey": response}
  115. except pyu2f.errors.U2FError as e:
  116. if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
  117. sys.stderr.write("Ineligible security key.\n")
  118. elif e.code == pyu2f.errors.U2FError.TIMEOUT:
  119. sys.stderr.write("Timed out while waiting for security key touch.\n")
  120. else:
  121. raise e
  122. except pyu2f.errors.NoDeviceFoundError:
  123. sys.stderr.write("No security key found.\n")
  124. return None
  125. class SamlChallenge(ReauthChallenge):
  126. """Challenge that asks the users to browse to their ID Providers.
  127. Currently SAML challenge is not supported. When obtaining the challenge
  128. input, exception will be raised to instruct the users to run
  129. `gcloud auth login` for reauthentication.
  130. """
  131. @property
  132. def name(self):
  133. return "SAML"
  134. @property
  135. def is_locally_eligible(self):
  136. return True
  137. def obtain_challenge_input(self, metadata):
  138. # Magic Arch has not fully supported returning a proper dedirect URL
  139. # for programmatic SAML users today. So we error our here and request
  140. # users to use gcloud to complete a login.
  141. raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE)
  142. AVAILABLE_CHALLENGES = {
  143. challenge.name: challenge
  144. for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
  145. }