blob: 95e76cb3262bc3f8c99642de7ad6b0d596b908e4 [file] [log] [blame]
arithmetic172882293fe2021-04-14 11:22:13 -07001# 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
arithmetic17285bd5ccf2021-10-21 15:25:46 -070023import six
24
arithmetic172882293fe2021-04-14 11:22:13 -070025from google.auth import _helpers
26from google.auth import exceptions
27
28
29REAUTH_ORIGIN = "https://accounts.google.com"
arithmetic172813aed5f2021-09-07 16:24:45 -070030SAML_CHALLENGE_MESSAGE = (
31 "Please run `gcloud auth login` to complete reauthentication with SAML."
32)
arithmetic172882293fe2021-04-14 11:22:13 -070033
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
arithmetic17285bd5ccf2021-10-21 15:25:46 -070050@six.add_metaclass(abc.ABCMeta)
51class ReauthChallenge(object):
arithmetic172882293fe2021-04-14 11:22:13 -070052 """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
arithmetic172813aed5f2021-09-07 16:24:45 -0700157class 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
arithmetic172882293fe2021-04-14 11:22:13 -0700180AVAILABLE_CHALLENGES = {
181 challenge.name: challenge
arithmetic172813aed5f2021-09-07 16:24:45 -0700182 for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
arithmetic172882293fe2021-04-14 11:22:13 -0700183}