blob: d0b070eda67012db1f07cbf9db691fd8fd4a353e [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
23import six
24
25from google.auth import _helpers
26from google.auth import exceptions
27
28
29REAUTH_ORIGIN = "https://accounts.google.com"
30
31
32def get_user_password(text):
33 """Get password from user.
34
35 Override this function with a different logic if you are using this library
36 outside a CLI.
37
38 Args:
39 text (str): message for the password prompt.
40
41 Returns:
42 str: password string.
43 """
44 return getpass.getpass(text)
45
46
47@six.add_metaclass(abc.ABCMeta)
48class ReauthChallenge(object):
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
115 import pyu2f.errors
116 import pyu2f.model
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 app_id = sk["applicationId"]
125
126 challenge_data = []
127 for c in challenges:
128 kh = c["keyHandle"].encode("ascii")
129 key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
130 challenge = c["challenge"].encode("ascii")
131 challenge = base64.urlsafe_b64decode(challenge)
132 challenge_data.append({"key": key, "challenge": challenge})
133
134 try:
135 api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
136 REAUTH_ORIGIN
137 )
138 response = api.Authenticate(
139 app_id, challenge_data, print_callback=sys.stderr.write
140 )
141 return {"securityKey": response}
142 except pyu2f.errors.U2FError as e:
143 if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
144 sys.stderr.write("Ineligible security key.\n")
145 elif e.code == pyu2f.errors.U2FError.TIMEOUT:
146 sys.stderr.write("Timed out while waiting for security key touch.\n")
147 else:
148 raise e
149 except pyu2f.errors.NoDeviceFoundError:
150 sys.stderr.write("No security key found.\n")
151 return None
152
153
154AVAILABLE_CHALLENGES = {
155 challenge.name: challenge
156 for challenge in [SecurityKeyChallenge(), PasswordChallenge()]
157}