challenges.py 5.0 KB

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