1515"""Tests for the reauth module."""
1616
1717import base64
18+ import os
1819import sys
1920
2021import mock
2324
2425from google .auth import exceptions
2526from google .oauth2 import challenges
27+ from google .oauth2 .webauthn_types import (
28+ AuthenticationExtensionsClientInputs ,
29+ AuthenticatorAssertionResponse ,
30+ GetRequest ,
31+ GetResponse ,
32+ PublicKeyCredentialDescriptor ,
33+ )
2634
2735
2836def test_get_user_password ():
@@ -54,6 +62,8 @@ def test_security_key():
5462
5563 # Test the case that security key challenge is passed with applicationId and
5664 # relyingPartyId the same.
65+ os .environ .pop ('"GOOGLE_AUTH_WEBAUTHN_PLUGIN"' , None )
66+
5767 with mock .patch ("pyu2f.model.RegisteredKey" , return_value = mock_key ):
5868 with mock .patch (
5969 "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
@@ -70,6 +80,19 @@ def test_security_key():
7080 print_callback = sys .stderr .write ,
7181 )
7282
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+
7396 # Test the case that security key challenge is passed with applicationId and
7497 # relyingPartyId different, first call works.
7598 metadata ["securityKey" ]["relyingPartyId" ] = "security_key_relying_party_id"
@@ -173,6 +196,136 @@ def test_security_key():
173196 assert excinfo .match (r"pyu2f dependency is required" )
174197
175198
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+
176329@mock .patch ("getpass.getpass" , return_value = "foo" )
177330def test_password_challenge (getpass_mock ):
178331 challenge = challenges .PasswordChallenge ()
0 commit comments