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"""Tests for the reauth module."""
16
17import base64
18import sys
19
20import mock
21import pytest
22import pyu2f
23
24from google.auth import exceptions
25from google.oauth2 import challenges
26
27
28def test_get_user_password():
29    with mock.patch("getpass.getpass", return_value="foo"):
30        assert challenges.get_user_password("") == "foo"
31
32
33def test_security_key():
34    metadata = {
35        "status": "READY",
36        "challengeId": 2,
37        "challengeType": "SECURITY_KEY",
38        "securityKey": {
39            "applicationId": "security_key_application_id",
40            "challenges": [
41                {
42                    "keyHandle": "some_key",
43                    "challenge": base64.urlsafe_b64encode(
44                        "some_challenge".encode("ascii")
45                    ).decode("ascii"),
46                }
47            ],
48        },
49    }
50    mock_key = mock.Mock()
51
52    challenge = challenges.SecurityKeyChallenge()
53
54    # Test the case that security key challenge is passed.
55    with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
56        with mock.patch(
57            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
58        ) as mock_authenticate:
59            mock_authenticate.return_value = "security key response"
60            assert challenge.name == "SECURITY_KEY"
61            assert challenge.is_locally_eligible
62            assert challenge.obtain_challenge_input(metadata) == {
63                "securityKey": "security key response"
64            }
65            mock_authenticate.assert_called_with(
66                "security_key_application_id",
67                [{"key": mock_key, "challenge": b"some_challenge"}],
68                print_callback=sys.stderr.write,
69            )
70
71    # Test various types of exceptions.
72    with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
73        with mock.patch(
74            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
75        ) as mock_authenticate:
76            mock_authenticate.side_effect = pyu2f.errors.U2FError(
77                pyu2f.errors.U2FError.DEVICE_INELIGIBLE
78            )
79            assert challenge.obtain_challenge_input(metadata) is None
80
81        with mock.patch(
82            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
83        ) as mock_authenticate:
84            mock_authenticate.side_effect = pyu2f.errors.U2FError(
85                pyu2f.errors.U2FError.TIMEOUT
86            )
87            assert challenge.obtain_challenge_input(metadata) is None
88
89        with mock.patch(
90            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
91        ) as mock_authenticate:
92            mock_authenticate.side_effect = pyu2f.errors.U2FError(
93                pyu2f.errors.U2FError.BAD_REQUEST
94            )
95            with pytest.raises(pyu2f.errors.U2FError):
96                challenge.obtain_challenge_input(metadata)
97
98        with mock.patch(
99            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
100        ) as mock_authenticate:
101            mock_authenticate.side_effect = pyu2f.errors.NoDeviceFoundError()
102            assert challenge.obtain_challenge_input(metadata) is None
103
104        with mock.patch(
105            "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
106        ) as mock_authenticate:
107            mock_authenticate.side_effect = pyu2f.errors.UnsupportedVersionException()
108            with pytest.raises(pyu2f.errors.UnsupportedVersionException):
109                challenge.obtain_challenge_input(metadata)
110
111        with mock.patch.dict("sys.modules"):
112            sys.modules["pyu2f"] = None
113            with pytest.raises(exceptions.ReauthFailError) as excinfo:
114                challenge.obtain_challenge_input(metadata)
115            assert excinfo.match(r"pyu2f dependency is required")
116
117
118@mock.patch("getpass.getpass", return_value="foo")
119def test_password_challenge(getpass_mock):
120    challenge = challenges.PasswordChallenge()
121
122    with mock.patch("getpass.getpass", return_value="foo"):
123        assert challenge.is_locally_eligible
124        assert challenge.name == "PASSWORD"
125        assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
126            "credential": "foo"
127        }
128
129    with mock.patch("getpass.getpass", return_value=None):
130        assert challenges.PasswordChallenge().obtain_challenge_input({}) == {
131            "credential": " "
132        }
133
134
135def test_saml_challenge():
136    challenge = challenges.SamlChallenge()
137    assert challenge.is_locally_eligible
138    assert challenge.name == "SAML"
139    with pytest.raises(exceptions.ReauthSamlChallengeFailError):
140        challenge.obtain_challenge_input(None)
141