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
23import six
24
25from google.auth import _helpers
26from google.auth import exceptions
27
28
29REAUTH_ORIGIN = "https://accounts.google.com"
30SAML_CHALLENGE_MESSAGE = (
31    "Please run `gcloud auth login` to complete reauthentication with SAML."
32)
33
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
50@six.add_metaclass(abc.ABCMeta)
51class ReauthChallenge(object):
52    """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
157class 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
180AVAILABLE_CHALLENGES = {
181    challenge.name: challenge
182    for challenge in [SecurityKeyChallenge(), PasswordChallenge(), SamlChallenge()]
183}
184