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