blob: 7756a8057773d3294d929f5fc9d9a69d56b5f45d [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
arithmetic172882293fe2021-04-14 11:22:13 -070023from google.auth import _helpers
24from google.auth import exceptions
25
26
27REAUTH_ORIGIN = "https://accounts.google.com"
28
29
30def get_user_password(text):
31 """Get password from user.
32
33 Override this function with a different logic if you are using this library
34 outside a CLI.
35
36 Args:
37 text (str): message for the password prompt.
38
39 Returns:
40 str: password string.
41 """
42 return getpass.getpass(text)
43
44
Tres Seaver560cf1e2021-08-03 16:35:54 -040045class ReauthChallenge(object, metaclass=abc.ABCMeta):
arithmetic172882293fe2021-04-14 11:22:13 -070046 """Base class for reauth challenges."""
47
48 @property
49 @abc.abstractmethod
50 def name(self): # pragma: NO COVER
51 """Returns the name of the challenge."""
52 raise NotImplementedError("name property must be implemented")
53
54 @property
55 @abc.abstractmethod
56 def is_locally_eligible(self): # pragma: NO COVER
57 """Returns true if a challenge is supported locally on this machine."""
58 raise NotImplementedError("is_locally_eligible property must be implemented")
59
60 @abc.abstractmethod
61 def obtain_challenge_input(self, metadata): # pragma: NO COVER
62 """Performs logic required to obtain credentials and returns it.
63
64 Args:
65 metadata (Mapping): challenge metadata returned in the 'challenges' field in
66 the initial reauth request. Includes the 'challengeType' field
67 and other challenge-specific fields.
68
69 Returns:
70 response that will be send to the reauth service as the content of
71 the 'proposalResponse' field in the request body. Usually a dict
72 with the keys specific to the challenge. For example,
73 ``{'credential': password}`` for password challenge.
74 """
75 raise NotImplementedError("obtain_challenge_input method must be implemented")
76
77
78class PasswordChallenge(ReauthChallenge):
79 """Challenge that asks for user's password."""
80
81 @property
82 def name(self):
83 return "PASSWORD"
84
85 @property
86 def is_locally_eligible(self):
87 return True
88
89 @_helpers.copy_docstring(ReauthChallenge)
90 def obtain_challenge_input(self, unused_metadata):
91 passwd = get_user_password("Please enter your password:")
92 if not passwd:
93 passwd = " " # avoid the server crashing in case of no password :D
94 return {"credential": passwd}
95
96
97class SecurityKeyChallenge(ReauthChallenge):
98 """Challenge that asks for user's security key touch."""
99
100 @property
101 def name(self):
102 return "SECURITY_KEY"
103
104 @property
105 def is_locally_eligible(self):
106 return True
107
108 @_helpers.copy_docstring(ReauthChallenge)
109 def obtain_challenge_input(self, metadata):
110 try:
111 import pyu2f.convenience.authenticator
112 import pyu2f.errors
113 import pyu2f.model
114 except ImportError:
115 raise exceptions.ReauthFailError(
116 "pyu2f dependency is required to use Security key reauth feature. "
117 "It can be installed via `pip install pyu2f` or `pip install google-auth[reauth]`."
118 )
119 sk = metadata["securityKey"]
120 challenges = sk["challenges"]
121 app_id = sk["applicationId"]
122
123 challenge_data = []
124 for c in challenges:
125 kh = c["keyHandle"].encode("ascii")
126 key = pyu2f.model.RegisteredKey(bytearray(base64.urlsafe_b64decode(kh)))
127 challenge = c["challenge"].encode("ascii")
128 challenge = base64.urlsafe_b64decode(challenge)
129 challenge_data.append({"key": key, "challenge": challenge})
130
131 try:
132 api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
133 REAUTH_ORIGIN
134 )
135 response = api.Authenticate(
136 app_id, challenge_data, print_callback=sys.stderr.write
137 )
138 return {"securityKey": response}
139 except pyu2f.errors.U2FError as e:
140 if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
141 sys.stderr.write("Ineligible security key.\n")
142 elif e.code == pyu2f.errors.U2FError.TIMEOUT:
143 sys.stderr.write("Timed out while waiting for security key touch.\n")
144 else:
145 raise e
146 except pyu2f.errors.NoDeviceFoundError:
147 sys.stderr.write("No security key found.\n")
148 return None
149
150
151AVAILABLE_CHALLENGES = {
152 challenge.name: challenge
153 for challenge in [SecurityKeyChallenge(), PasswordChallenge()]
154}