Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/oauth2/challenges.py: 44%

91 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

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 

23from google.auth import _helpers 

24from google.auth import exceptions 

25 

26 

27REAUTH_ORIGIN = "https://accounts.google.com" 

28SAML_CHALLENGE_MESSAGE = ( 

29 "Please run `gcloud auth login` to complete reauthentication with SAML." 

30) 

31 

32 

33def get_user_password(text): 

34 """Get password from user. 

35 

36 Override this function with a different logic if you are using this library 

37 outside a CLI. 

38 

39 Args: 

40 text (str): message for the password prompt. 

41 

42 Returns: 

43 str: password string. 

44 """ 

45 return getpass.getpass(text) 

46 

47 

48class ReauthChallenge(metaclass=abc.ABCMeta): 

49 """Base class for reauth challenges.""" 

50 

51 @property 

52 @abc.abstractmethod 

53 def name(self): # pragma: NO COVER 

54 """Returns the name of the challenge.""" 

55 raise NotImplementedError("name property must be implemented") 

56 

57 @property 

58 @abc.abstractmethod 

59 def is_locally_eligible(self): # pragma: NO COVER 

60 """Returns true if a challenge is supported locally on this machine.""" 

61 raise NotImplementedError("is_locally_eligible property must be implemented") 

62 

63 @abc.abstractmethod 

64 def obtain_challenge_input(self, metadata): # pragma: NO COVER 

65 """Performs logic required to obtain credentials and returns it. 

66 

67 Args: 

68 metadata (Mapping): challenge metadata returned in the 'challenges' field in 

69 the initial reauth request. Includes the 'challengeType' field 

70 and other challenge-specific fields. 

71 

72 Returns: 

73 response that will be send to the reauth service as the content of 

74 the 'proposalResponse' field in the request body. Usually a dict 

75 with the keys specific to the challenge. For example, 

76 ``{'credential': password}`` for password challenge. 

77 """ 

78 raise NotImplementedError("obtain_challenge_input method must be implemented") 

79 

80 

81class PasswordChallenge(ReauthChallenge): 

82 """Challenge that asks for user's password.""" 

83 

84 @property 

85 def name(self): 

86 return "PASSWORD" 

87 

88 @property 

89 def is_locally_eligible(self): 

90 return True 

91 

92 @_helpers.copy_docstring(ReauthChallenge) 

93 def obtain_challenge_input(self, unused_metadata): 

94 passwd = get_user_password("Please enter your password:") 

95 if not passwd: 

96 passwd = " " # avoid the server crashing in case of no password :D 

97 return {"credential": passwd} 

98 

99 

100class SecurityKeyChallenge(ReauthChallenge): 

101 """Challenge that asks for user's security key touch.""" 

102 

103 @property 

104 def name(self): 

105 return "SECURITY_KEY" 

106 

107 @property 

108 def is_locally_eligible(self): 

109 return True 

110 

111 @_helpers.copy_docstring(ReauthChallenge) 

112 def obtain_challenge_input(self, metadata): 

113 try: 

114 import pyu2f.convenience.authenticator # type: ignore 

115 import pyu2f.errors # type: ignore 

116 import pyu2f.model # type: ignore 

117 except ImportError: 

118 raise exceptions.ReauthFailError( 

119 "pyu2f dependency is required to use Security key reauth feature. " 

120 "It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`." 

121 ) 

122 sk = metadata["securityKey"] 

123 challenges = sk["challenges"] 

124 # Read both 'applicationId' and 'relyingPartyId', if they are the same, use 

125 # applicationId, if they are different, use relyingPartyId first and retry 

126 # with applicationId 

127 application_id = sk["applicationId"] 

128 relying_party_id = sk["relyingPartyId"] 

129 

130 if application_id != relying_party_id: 

131 application_parameters = [relying_party_id, application_id] 

132 else: 

133 application_parameters = [application_id] 

134 

135 challenge_data = [] 

136 for c in challenges: 

137 kh = c["keyHandle"].encode("ascii") 

138 key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh))) 

139 challenge = c["challenge"].encode("ascii") 

140 challenge = base64.urlsafe_b64decode(challenge) 

141 challenge_data.append({"key": key, "challenge": challenge}) 

142 

143 # Track number of tries to suppress error message until all application_parameters 

144 # are tried. 

145 tries = 0 

146 for app_id in application_parameters: 

147 try: 

148 tries += 1 

149 api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator( 

150 REAUTH_ORIGIN 

151 ) 

152 response = api.Authenticate( 

153 app_id, challenge_data, print_callback=sys.stderr.write 

154 ) 

155 return {"securityKey": response} 

156 except pyu2f.errors.U2FError as e: 

157 if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE: 

158 # Only show error if all app_ids have been tried 

159 if tries == len(application_parameters): 

160 sys.stderr.write("Ineligible security key.\n") 

161 return None 

162 continue 

163 if e.code == pyu2f.errors.U2FError.TIMEOUT: 

164 sys.stderr.write( 

165 "Timed out while waiting for security key touch.\n" 

166 ) 

167 else: 

168 raise e 

169 except pyu2f.errors.PluginError as e: 

170 sys.stderr.write("Plugin error: {}.\n".format(e)) 

171 continue 

172 except pyu2f.errors.NoDeviceFoundError: 

173 sys.stderr.write("No security key found.\n") 

174 return None 

175 

176 

177class SamlChallenge(ReauthChallenge): 

178 """Challenge that asks the users to browse to their ID Providers. 

179 

180 Currently SAML challenge is not supported. When obtaining the challenge 

181 input, exception will be raised to instruct the users to run 

182 `gcloud auth login` for reauthentication. 

183 """ 

184 

185 @property 

186 def name(self): 

187 return "SAML" 

188 

189 @property 

190 def is_locally_eligible(self): 

191 return True 

192 

193 def obtain_challenge_input(self, metadata): 

194 # Magic Arch has not fully supported returning a proper dedirect URL 

195 # for programmatic SAML users today. So we error our here and request 

196 # users to use gcloud to complete a login. 

197 raise exceptions.ReauthSamlChallengeFailError(SAML_CHALLENGE_MESSAGE) 

198 

199 

200AVAILABLE_CHALLENGES = { 

201 challenge.name: challenge 

202 for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()] 

203}