@@ -2,6 +2,7 @@ package controllers_test
22
33import (
44 "encoding/hex"
5+ "encoding/json"
56 "net/http"
67 "net/http/httptest"
78 "testing"
@@ -13,7 +14,9 @@ import (
1314 "github.com/brave/accounts/services"
1415 "github.com/brave/accounts/util"
1516 "github.com/bytemare/opaque"
17+ "github.com/descope/virtualwebauthn"
1618 "github.com/go-chi/chi/v5"
19+ "github.com/go-webauthn/webauthn/protocol"
1720 "github.com/golang-jwt/jwt/v5"
1821 "github.com/google/uuid"
1922 "github.com/pquerna/otp"
@@ -27,6 +30,7 @@ type AuthTestSuite struct {
2730 ds * datastore.Datastore
2831 keyServiceDs * datastore.Datastore
2932 jwtService * services.JWTService
33+ twoFAService * services.TwoFAService
3034 account * datastore.Account
3135 controller * controllers.AuthController
3236 router * chi.Mux
@@ -65,8 +69,8 @@ func (suite *AuthTestSuite) SetupTest() {
6569 suite .Require ().NoError (err )
6670 opaqueService , err := services .NewOpaqueService (suite .ds , false )
6771 suite .Require ().NoError (err )
68- twoFAService : = services .NewTwoFAService (suite .ds , false )
69- suite .controller = controllers .NewAuthController (opaqueService , suite .jwtService , twoFAService , suite .ds , & MockSESService {})
72+ suite . twoFAService = services .NewTwoFAService (suite .ds , false )
73+ suite .controller = controllers .NewAuthController (opaqueService , suite .jwtService , suite . twoFAService , suite .ds , & MockSESService {})
7074
7175 suite .account , err = suite .ds .GetOrCreateAccount ("test@example.com" )
7276 suite .Require ().NoError (err )
@@ -245,7 +249,7 @@ func (suite *AuthTestSuite) performLoginSteps() (*controllers.LoginFinalizeRespo
245249func (suite * AuthTestSuite ) TestAuthLogin () {
246250 finalizeResp , _ := suite .performLoginSteps ()
247251 suite .NotEmpty (finalizeResp .AuthToken )
248- suite .False (finalizeResp .RequiresTwoFA )
252+ suite .Nil (finalizeResp .TwoFAOptions )
249253
250254 req := httptest .NewRequest ("GET" , "/v2/auth/validate" , nil )
251255 req .Header .Add ("Authorization" , "Bearer " + * finalizeResp .AuthToken )
@@ -484,7 +488,9 @@ func (suite *AuthTestSuite) TestAuth2FAWithTOTPCode() {
484488 finalizeResp , loginToken := suite .performLoginSteps ()
485489
486490 // Verify we need 2FA
487- suite .True (finalizeResp .RequiresTwoFA )
491+ suite .Require ().NotNil (finalizeResp .TwoFAOptions )
492+ suite .True (finalizeResp .TwoFAOptions .TOTPEnabled )
493+ suite .Nil (finalizeResp .TwoFAOptions .WebAuthnRequest )
488494 suite .Nil (finalizeResp .AuthToken )
489495
490496 // Try using invalid TOTP code first
@@ -520,7 +526,9 @@ func (suite *AuthTestSuite) TestAuth2FAWithTOTPCode() {
520526
521527 // Perform login steps again to get a new login state
522528 finalizeResp , loginToken = suite .performLoginSteps ()
523- suite .True (finalizeResp .RequiresTwoFA )
529+ suite .Require ().NotNil (finalizeResp .TwoFAOptions )
530+ suite .True (finalizeResp .TwoFAOptions .TOTPEnabled )
531+ suite .Nil (finalizeResp .TwoFAOptions .WebAuthnRequest )
524532 suite .Nil (finalizeResp .AuthToken )
525533
526534 // Try to reuse the same code with the new login state
@@ -541,6 +549,7 @@ func (suite *AuthTestSuite) TestAuth2FAWithTOTPCode() {
541549 account , err := suite .ds .GetOrCreateAccount (suite .account .Email )
542550 suite .Require ().NoError (err )
543551 suite .True (account .TOTPEnabled )
552+ suite .False (account .WebAuthnEnabled )
544553 suite .NotNil (account .RecoveryKeyHash )
545554}
546555
@@ -568,7 +577,9 @@ func (suite *AuthTestSuite) TestAuth2FAWithRecoveryKey() {
568577 finalizeResp , loginToken := suite .performLoginSteps ()
569578
570579 // Verify we need 2FA
571- suite .True (finalizeResp .RequiresTwoFA )
580+ suite .Require ().NotNil (finalizeResp .TwoFAOptions )
581+ suite .True (finalizeResp .TwoFAOptions .TOTPEnabled )
582+ suite .Nil (finalizeResp .TwoFAOptions .WebAuthnRequest )
572583 suite .Nil (finalizeResp .AuthToken )
573584
574585 // Test 2FA with bad recovery key
@@ -606,10 +617,11 @@ func (suite *AuthTestSuite) TestAuth2FAWithRecoveryKey() {
606617 account , err := suite .ds .GetOrCreateAccount (suite .account .Email )
607618 suite .Require ().NoError (err )
608619 suite .False (account .TOTPEnabled )
620+ suite .False (account .WebAuthnEnabled )
609621 suite .Nil (account .RecoveryKeyHash )
610622
611623 finalizeResp , _ = suite .performLoginSteps ()
612- suite .False (finalizeResp .RequiresTwoFA )
624+ suite .Nil (finalizeResp .TwoFAOptions )
613625 suite .NotNil (finalizeResp .AuthToken )
614626
615627 validateReq = httptest .NewRequest ("GET" , "/v2/auth/validate" , nil )
@@ -618,6 +630,78 @@ func (suite *AuthTestSuite) TestAuth2FAWithRecoveryKey() {
618630 suite .Equal (http .StatusOK , validateResp .Code )
619631}
620632
633+ func (suite * AuthTestSuite ) TestAuth2FAWithWebAuthn () {
634+ // Add a WebAuthn credential for the account
635+ authenticator := virtualwebauthn .NewAuthenticator ()
636+ registeredCredential := addWebAuthnCredential (suite .T (), suite .twoFAService , suite .ds , authenticator , suite .account , "YubiKey 5C" )
637+
638+ // Create an unregistered credential for testing failure case
639+ unregisteredCredential := virtualwebauthn .NewCredential (virtualwebauthn .KeyTypeEC2 )
640+
641+ // Perform login initialization and finalization
642+ finalizeResp , loginToken := suite .performLoginSteps ()
643+
644+ // Verify we need 2FA
645+ suite .Require ().NotNil (finalizeResp .TwoFAOptions )
646+ suite .Require ().NotNil (finalizeResp .TwoFAOptions .WebAuthnRequest )
647+ suite .Nil (finalizeResp .AuthToken )
648+
649+ // Try with unregistered credential first
650+ rp := createWebAuthnRelyingParty ()
651+
652+ assertionOptionsJSON , err := json .Marshal (finalizeResp .TwoFAOptions .WebAuthnRequest )
653+ suite .Require ().NoError (err )
654+
655+ parsedAssertionOptions , err := virtualwebauthn .ParseAssertionOptions (string (assertionOptionsJSON ))
656+ suite .Require ().NoError (err )
657+
658+ // Use unregistered credential - should fail
659+ unregisteredAssertionResponse := virtualwebauthn .CreateAssertionResponse (rp , authenticator , unregisteredCredential , * parsedAssertionOptions )
660+
661+ var unregisteredWebAuthnResponse protocol.CredentialAssertionResponse
662+ err = json .Unmarshal ([]byte (unregisteredAssertionResponse ), & unregisteredWebAuthnResponse )
663+ suite .Require ().NoError (err )
664+
665+ invalidRequest := services.TwoFAAuthRequest {
666+ WebAuthnResponse : & unregisteredWebAuthnResponse ,
667+ }
668+ req := util .CreateJSONTestRequest ("/v2/auth/login/finalize_2fa" , invalidRequest )
669+ req .Header .Set ("Authorization" , "Bearer " + loginToken )
670+ resp := util .ExecuteTestRequest (req , suite .router )
671+
672+ // Should get unauthorized with unregistered credential
673+ suite .Equal (http .StatusUnauthorized , resp .Code )
674+ util .AssertErrorResponseCode (suite .T (), resp , util .ErrBadWebAuthnResponse .Code )
675+
676+ // Now use the registered credential - should succeed
677+ registeredAssertionResponse := virtualwebauthn .CreateAssertionResponse (rp , authenticator , registeredCredential , * parsedAssertionOptions )
678+
679+ var registeredWebAuthnResponse protocol.CredentialAssertionResponse
680+ err = json .Unmarshal ([]byte (registeredAssertionResponse ), & registeredWebAuthnResponse )
681+ suite .Require ().NoError (err )
682+
683+ validRequest := services.TwoFAAuthRequest {
684+ WebAuthnResponse : & registeredWebAuthnResponse ,
685+ }
686+ req = util .CreateJSONTestRequest ("/v2/auth/login/finalize_2fa" , validRequest )
687+ req .Header .Set ("Authorization" , "Bearer " + loginToken )
688+ resp = util .ExecuteTestRequest (req , suite .router )
689+
690+ // Should succeed with registered credential
691+ suite .Equal (http .StatusOK , resp .Code )
692+
693+ var parsedTwoFAResp controllers.LoginFinalize2FAResponse
694+ util .DecodeJSONTestResponse (suite .T (), resp .Body , & parsedTwoFAResp )
695+ suite .NotEmpty (parsedTwoFAResp .AuthToken )
696+ suite .False (parsedTwoFAResp .TwoFADisabled )
697+
698+ // Verify the auth token works
699+ validateReq := httptest .NewRequest ("GET" , "/v2/auth/validate" , nil )
700+ validateReq .Header .Add ("Authorization" , "Bearer " + parsedTwoFAResp .AuthToken )
701+ validateResp := util .ExecuteTestRequest (validateReq , suite .router )
702+ suite .Equal (http .StatusOK , validateResp .Code )
703+ }
704+
621705func TestAuthTestSuite (t * testing.T ) {
622706 t .Run ("NoKeyService" , func (t * testing.T ) {
623707 suite .Run (t , NewAuthTestSuite (false ))
0 commit comments