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
« 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.
15""" Challenges for reauthentication.
16"""
18import abc
19import base64
20import getpass
21import sys
23from google.auth import _helpers
24from google.auth import exceptions
27REAUTH_ORIGIN = "https://accounts.google.com"
28SAML_CHALLENGE_MESSAGE = (
29 "Please run `gcloud auth login` to complete reauthentication with SAML."
30)
33def get_user_password(text):
34 """Get password from user.
36 Override this function with a different logic if you are using this library
37 outside a CLI.
39 Args:
40 text (str): message for the password prompt.
42 Returns:
43 str: password string.
44 """
45 return getpass.getpass(text)
48class ReauthChallenge(metaclass=abc.ABCMeta):
49 """Base class for reauth challenges."""
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")
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")
63 @abc.abstractmethod
64 def obtain_challenge_input(self, metadata): # pragma: NO COVER
65 """Performs logic required to obtain credentials and returns it.
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.
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")
81class PasswordChallenge(ReauthChallenge):
82 """Challenge that asks for user's password."""
84 @property
85 def name(self):
86 return "PASSWORD"
88 @property
89 def is_locally_eligible(self):
90 return True
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}
100class SecurityKeyChallenge(ReauthChallenge):
101 """Challenge that asks for user's security key touch."""
103 @property
104 def name(self):
105 return "SECURITY_KEY"
107 @property
108 def is_locally_eligible(self):
109 return True
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"]
130 if application_id != relying_party_id:
131 application_parameters = [relying_party_id, application_id]
132 else:
133 application_parameters = [application_id]
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})
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
177class SamlChallenge(ReauthChallenge):
178 """Challenge that asks the users to browse to their ID Providers.
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 """
185 @property
186 def name(self):
187 return "SAML"
189 @property
190 def is_locally_eligible(self):
191 return True
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)
200AVAILABLE_CHALLENGES = {
201 challenge.name: challenge
202 for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
203}