15
15
"""Tests for the reauth module."""
16
16
17
17
import base64
18
+ import os
18
19
import sys
19
20
20
21
import mock
23
24
24
25
from google .auth import exceptions
25
26
from google .oauth2 import challenges
27
+ from google .oauth2 .webauthn_types import (
28
+ AuthenticationExtensionsClientInputs ,
29
+ AuthenticatorAssertionResponse ,
30
+ GetRequest ,
31
+ GetResponse ,
32
+ PublicKeyCredentialDescriptor ,
33
+ )
26
34
27
35
28
36
def test_get_user_password ():
@@ -54,6 +62,8 @@ def test_security_key():
54
62
55
63
# Test the case that security key challenge is passed with applicationId and
56
64
# relyingPartyId the same.
65
+ os .environ .pop ('"GOOGLE_AUTH_WEBAUTHN_PLUGIN"' , None )
66
+
57
67
with mock .patch ("pyu2f.model.RegisteredKey" , return_value = mock_key ):
58
68
with mock .patch (
59
69
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
@@ -70,6 +80,19 @@ def test_security_key():
70
80
print_callback = sys .stderr .write ,
71
81
)
72
82
83
+ # Test the case that webauthn plugin is available
84
+ os .environ ["GOOGLE_AUTH_WEBAUTHN_PLUGIN" ] = "plugin"
85
+
86
+ with mock .patch (
87
+ "google.oauth2.challenges.SecurityKeyChallenge._obtain_challenge_input_webauthn" ,
88
+ return_value = {"securityKey" : "security key response" },
89
+ ):
90
+
91
+ assert challenge .obtain_challenge_input (metadata ) == {
92
+ "securityKey" : "security key response"
93
+ }
94
+ os .environ .pop ('"GOOGLE_AUTH_WEBAUTHN_PLUGIN"' , None )
95
+
73
96
# Test the case that security key challenge is passed with applicationId and
74
97
# relyingPartyId different, first call works.
75
98
metadata ["securityKey" ]["relyingPartyId" ] = "security_key_relying_party_id"
@@ -173,6 +196,136 @@ def test_security_key():
173
196
assert excinfo .match (r"pyu2f dependency is required" )
174
197
175
198
199
+ def test_security_key_webauthn ():
200
+ metadata = {
201
+ "status" : "READY" ,
202
+ "challengeId" : 2 ,
203
+ "challengeType" : "SECURITY_KEY" ,
204
+ "securityKey" : {
205
+ "applicationId" : "security_key_application_id" ,
206
+ "challenges" : [
207
+ {
208
+ "keyHandle" : "some_key" ,
209
+ "challenge" : base64 .urlsafe_b64encode (
210
+ "some_challenge" .encode ("ascii" )
211
+ ).decode ("ascii" ),
212
+ }
213
+ ],
214
+ "relyingPartyId" : "security_key_application_id" ,
215
+ },
216
+ }
217
+
218
+ challenge = challenges .SecurityKeyChallenge ()
219
+
220
+ sk = metadata ["securityKey" ]
221
+ sk_challenges = sk ["challenges" ]
222
+
223
+ application_id = sk ["applicationId" ]
224
+
225
+ allow_credentials = []
226
+ for sk_challenge in sk_challenges :
227
+ allow_credentials .append (
228
+ PublicKeyCredentialDescriptor (id = sk_challenge ["keyHandle" ])
229
+ )
230
+
231
+ extension = AuthenticationExtensionsClientInputs (appid = application_id )
232
+
233
+ get_request = GetRequest (
234
+ origin = challenges .REAUTH_ORIGIN ,
235
+ rpid = application_id ,
236
+ challenge = challenge ._unpadded_urlsafe_b64recode (sk_challenge ["challenge" ]),
237
+ timeout_ms = challenges .WEBAUTHN_TIMEOUT_MS ,
238
+ allow_credentials = allow_credentials ,
239
+ user_verification = "required" ,
240
+ extensions = extension ,
241
+ )
242
+
243
+ assertion_resp = AuthenticatorAssertionResponse (
244
+ client_data_json = "clientDataJSON" ,
245
+ authenticator_data = "authenticatorData" ,
246
+ signature = "signature" ,
247
+ user_handle = "userHandle" ,
248
+ )
249
+ get_response = GetResponse (
250
+ id = "id" ,
251
+ response = assertion_resp ,
252
+ authenticator_attachment = "authenticatorAttachment" ,
253
+ client_extension_results = "clientExtensionResults" ,
254
+ )
255
+ response = {
256
+ "clientData" : get_response .response .client_data_json ,
257
+ "authenticatorData" : get_response .response .authenticator_data ,
258
+ "signatureData" : get_response .response .signature ,
259
+ "applicationId" : "security_key_application_id" ,
260
+ "keyHandle" : get_response .id ,
261
+ "securityKeyReplyType" : 2 ,
262
+ }
263
+
264
+ mock_handler = mock .Mock ()
265
+ mock_handler .get .return_value = get_response
266
+
267
+ # Test success case
268
+ assert challenge ._obtain_challenge_input_webauthn (metadata , mock_handler ) == {
269
+ "securityKey" : response
270
+ }
271
+ mock_handler .get .assert_called_with (get_request )
272
+
273
+ # Test exceptions
274
+
275
+ # Missing Values
276
+ sk = metadata ["securityKey" ]
277
+ metadata ["securityKey" ] = None
278
+ with pytest .raises (exceptions .InvalidValue ):
279
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
280
+ metadata ["securityKey" ] = sk
281
+
282
+ c = metadata ["securityKey" ]["challenges" ]
283
+ metadata ["securityKey" ]["challenges" ] = None
284
+ with pytest .raises (exceptions .InvalidValue ):
285
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
286
+ metadata ["securityKey" ]["challenges" ] = []
287
+ with pytest .raises (exceptions .InvalidValue ):
288
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
289
+ metadata ["securityKey" ]["challenges" ] = c
290
+
291
+ aid = metadata ["securityKey" ]["applicationId" ]
292
+ metadata ["securityKey" ]["applicationId" ] = None
293
+ with pytest .raises (exceptions .InvalidValue ):
294
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
295
+ metadata ["securityKey" ]["applicationId" ] = aid
296
+
297
+ rpi = metadata ["securityKey" ]["relyingPartyId" ]
298
+ metadata ["securityKey" ]["relyingPartyId" ] = None
299
+ with pytest .raises (exceptions .InvalidValue ):
300
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
301
+ metadata ["securityKey" ]["relyingPartyId" ] = rpi
302
+
303
+ kh = metadata ["securityKey" ]["challenges" ][0 ]["keyHandle" ]
304
+ metadata ["securityKey" ]["challenges" ][0 ]["keyHandle" ] = None
305
+ with pytest .raises (exceptions .InvalidValue ):
306
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
307
+ metadata ["securityKey" ]["challenges" ][0 ]["keyHandle" ] = kh
308
+
309
+ ch = metadata ["securityKey" ]["challenges" ][0 ]["challenge" ]
310
+ metadata ["securityKey" ]["challenges" ][0 ]["challenge" ] = None
311
+ with pytest .raises (exceptions .InvalidValue ):
312
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
313
+ metadata ["securityKey" ]["challenges" ][0 ]["challenge" ] = ch
314
+
315
+ # Handler Exceptions
316
+ mock_handler .get .side_effect = exceptions .MalformedError
317
+ with pytest .raises (exceptions .MalformedError ):
318
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
319
+
320
+ mock_handler .get .side_effect = exceptions .InvalidResource
321
+ with pytest .raises (exceptions .InvalidResource ):
322
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
323
+
324
+ mock_handler .get .side_effect = exceptions .ReauthFailError
325
+ with pytest .raises (exceptions .ReauthFailError ):
326
+ challenge ._obtain_challenge_input_webauthn (metadata , mock_handler )
327
+
328
+
176
329
@mock .patch ("getpass.getpass" , return_value = "foo" )
177
330
def test_password_challenge (getpass_mock ):
178
331
challenge = challenges .PasswordChallenge ()
0 commit comments