Skip to content

Commit 01ff4cc

Browse files
fix: Read both applicationId and relyingPartyId. (#1246)
If they are the same, use applicationId. If they are different, first use relyingPartyId and retry with applicationId. This allows security keys enrolled with both u2f and WebAuthN to work with the Reauth API. Co-authored-by: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com>
1 parent 69e9058 commit 01ff4cc

File tree

2 files changed

+100
-20
lines changed

2 files changed

+100
-20
lines changed

packages/google-auth/google/oauth2/challenges.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,16 @@ def obtain_challenge_input(self, metadata):
124124
)
125125
sk = metadata["securityKey"]
126126
challenges = sk["challenges"]
127-
app_id = sk["applicationId"]
127+
# Read both 'applicationId' and 'relyingPartyId', if they are the same, use
128+
# applicationId, if they are different, use relyingPartyId first and retry
129+
# with applicationId
130+
application_id = sk["applicationId"]
131+
relying_party_id = sk["relyingPartyId"]
132+
133+
if application_id != relying_party_id:
134+
application_parameters = [relying_party_id, application_id]
135+
else:
136+
application_parameters = [application_id]
128137

129138
challenge_data = []
130139
for c in challenges:
@@ -134,24 +143,37 @@ def obtain_challenge_input(self, metadata):
134143
challenge = base64.urlsafe_b64decode(challenge)
135144
challenge_data.append({"key": key, "challenge": challenge})
136145

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
146+
# Track number of tries to suppress error message until all application_parameters
147+
# are tried.
148+
tries = 0
149+
for app_id in application_parameters:
150+
try:
151+
tries += 1
152+
api = pyu2f.convenience.authenticator.CreateCompositeAuthenticator(
153+
REAUTH_ORIGIN
154+
)
155+
response = api.Authenticate(
156+
app_id, challenge_data, print_callback=sys.stderr.write
157+
)
158+
return {"securityKey": response}
159+
except pyu2f.errors.U2FError as e:
160+
if e.code == pyu2f.errors.U2FError.DEVICE_INELIGIBLE:
161+
# Only show error if all app_ids have been tried
162+
if tries == len(application_parameters):
163+
sys.stderr.write("Ineligible security key.\n")
164+
return None
165+
continue
166+
if e.code == pyu2f.errors.U2FError.TIMEOUT:
167+
sys.stderr.write(
168+
"Timed out while waiting for security key touch.\n"
169+
)
170+
else:
171+
raise e
172+
except pyu2f.errors.PluginError:
173+
continue
174+
except pyu2f.errors.NoDeviceFoundError:
175+
sys.stderr.write("No security key found.\n")
176+
return None
155177

156178

157179
class SamlChallenge(ReauthChallenge):

packages/google-auth/tests/oauth2/test_challenges.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ def test_security_key():
4545
).decode("ascii"),
4646
}
4747
],
48+
"relyingPartyId": "security_key_application_id",
4849
},
4950
}
5051
mock_key = mock.Mock()
5152

5253
challenge = challenges.SecurityKeyChallenge()
5354

54-
# Test the case that security key challenge is passed.
55+
# Test the case that security key challenge is passed with applicationId and
56+
# relyingPartyId the same.
5557
with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
5658
with mock.patch(
5759
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
@@ -68,6 +70,56 @@ def test_security_key():
6870
print_callback=sys.stderr.write,
6971
)
7072

73+
# Test the case that security key challenge is passed with applicationId and
74+
# relyingPartyId different, first call works.
75+
metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
76+
sys.stderr.write("metadata=" + str(metadata) + "\n")
77+
with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
78+
with mock.patch(
79+
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
80+
) as mock_authenticate:
81+
mock_authenticate.return_value = "security key response"
82+
assert challenge.name == "SECURITY_KEY"
83+
assert challenge.is_locally_eligible
84+
assert challenge.obtain_challenge_input(metadata) == {
85+
"securityKey": "security key response"
86+
}
87+
mock_authenticate.assert_called_with(
88+
"security_key_relying_party_id",
89+
[{"key": mock_key, "challenge": b"some_challenge"}],
90+
print_callback=sys.stderr.write,
91+
)
92+
93+
# Test the case that security key challenge is passed with applicationId and
94+
# relyingPartyId different, first call fails, requires retry.
95+
metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
96+
with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
97+
with mock.patch(
98+
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
99+
) as mock_authenticate:
100+
assert challenge.name == "SECURITY_KEY"
101+
assert challenge.is_locally_eligible
102+
mock_authenticate.side_effect = [
103+
pyu2f.errors.U2FError(pyu2f.errors.U2FError.DEVICE_INELIGIBLE),
104+
"security key response",
105+
]
106+
assert challenge.obtain_challenge_input(metadata) == {
107+
"securityKey": "security key response"
108+
}
109+
calls = [
110+
mock.call(
111+
"security_key_relying_party_id",
112+
[{"key": mock_key, "challenge": b"some_challenge"}],
113+
print_callback=sys.stderr.write,
114+
),
115+
mock.call(
116+
"security_key_application_id",
117+
[{"key": mock_key, "challenge": b"some_challenge"}],
118+
print_callback=sys.stderr.write,
119+
),
120+
]
121+
mock_authenticate.assert_has_calls(calls)
122+
71123
# Test various types of exceptions.
72124
with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
73125
with mock.patch(
@@ -86,6 +138,12 @@ def test_security_key():
86138
)
87139
assert challenge.obtain_challenge_input(metadata) is None
88140

141+
with mock.patch(
142+
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
143+
) as mock_authenticate:
144+
mock_authenticate.side_effect = pyu2f.errors.PluginError()
145+
assert challenge.obtain_challenge_input(metadata) is None
146+
89147
with mock.patch(
90148
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
91149
) as mock_authenticate:

0 commit comments

Comments
 (0)