package suites import ( "context" "log" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/authelia/authelia/v4/internal/duo" "github.com/authelia/authelia/v4/internal/models" "github.com/authelia/authelia/v4/internal/storage" ) type DuoPushWebDriverSuite struct { *RodSuite } func NewDuoPushWebDriverSuite() *DuoPushWebDriverSuite { return &DuoPushWebDriverSuite{RodSuite: new(RodSuite)} } func (s *DuoPushWebDriverSuite) SetupSuite() { browser, err := StartRod() if err != nil { log.Fatal(err) } s.RodSession = browser } func (s *DuoPushWebDriverSuite) TearDownSuite() { err := s.RodSession.Stop() if err != nil { log.Fatal(err) } } func (s *DuoPushWebDriverSuite) SetupTest() { s.Page = s.doCreateTab(s.T(), HomeBaseURL) s.verifyIsHome(s.T(), s.Page) } func (s *DuoPushWebDriverSuite) TearDownTest() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer func() { cancel() s.collectScreenshot(ctx.Err(), s.Page) s.collectCoverage(s.Page) s.MustClose() }() // Set default 2FA preference and clean up any Duo device already in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferred2FAMethod(ctx, "john", "totp")) require.NoError(s.T(), provider.DeletePreferredDuoDevice(ctx, "john")) } func (s *DuoPushWebDriverSuite) TestShouldBypassDeviceSelection() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "allow", StatusMessage: "Allowing unknown user", } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldDenyDeviceSelection() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "deny", StatusMessage: "We're sorry, access is not allowed.", } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Device selection was denied by Duo policy") } func (s *DuoPushWebDriverSuite) TestShouldAskUserToRegister() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "enroll", EnrollPortalURL: "https://api-example.duosecurity.com/portal?code=1234567890ABCDEF&akey=12345ABCDEFGHIJ67890", } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "state-not-registered") s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "No compatible device found") enrollPage := s.Page.MustWaitOpen() s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "register-link").MustClick() s.Page = enrollPage() assert.Contains(s.T(), s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "description").MustText(), "This enrollment code has expired. Contact your administrator to get a new enrollment code.") } func (s *DuoPushWebDriverSuite) TestShouldAutoSelectDevice() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }}, } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) // Authenticate. s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") // Switch Method where single Device should be selected automatically. s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyIsHome(s.T(), s.Context(ctx)) // Re-Login the user. s.doLogout(s.T(), s.Context(ctx)) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") // And check the latest method and device is still used. s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method") // Meaning the authentication is successful. s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldSelectDevice() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Set default 2FA preference to enable Select Device link in frontend. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"})) var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }, { Device: "1234567890ABCDEFGHIJ", DisplayName: "Test Device 2", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }}, } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) // Authenticate. s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") // Switch Method where Device Selection should open automatically. s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") // Check for available Device 1. s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "device-12345ABCDEFGHIJ67890") // Test Back button. s.doClickButton(s.T(), s.Context(ctx), "device-selection-back") // then select Device 2 for further use and be redirected. s.doChangeDevice(s.T(), s.Context(ctx), "1234567890ABCDEFGHIJ") s.verifyIsHome(s.T(), s.Context(ctx)) // Re-Login the user. s.doLogout(s.T(), s.Context(ctx)) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") // And check the latest method and device is still used. s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method") // Meaning the authentication is successful. s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldFailInitialSelectionBecauseOfUnsupportedMethod() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"auto", "sms"}, }}, } ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "state-not-registered") s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "No compatible device found") } func (s *DuoPushWebDriverSuite) TestShouldSelectNewDeviceAfterSavedDeviceMethodIsNoLongerSupported() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"push", "sms"}, }, { Device: "1234567890ABCDEFGHIJ", DisplayName: "Test Device 2", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }}, } // Setup unsupported Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "sms"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "device-selection") s.doSelectDevice(s.T(), s.Context(ctx), "12345ABCDEFGHIJ67890") s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldAutoSelectNewDeviceAfterSavedDeviceIsNoLongerAvailable() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"push", "sms"}, }}, } // Setup unsupported Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "ABCDEFGHIJ1234567890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionBypassed() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "allow", StatusMessage: "Allowing unknown user", } provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Deny) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.doClickButton(s.T(), s.Context(ctx), "selection-link") s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Device selection was bypassed by Duo policy") } func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionDenied() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "deny", StatusMessage: "We're sorry, access is not allowed.", } provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Deny) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") err := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "selection-link").Click("left") require.NoError(s.T(), err) s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Device selection was denied by Duo policy") } func (s *DuoPushWebDriverSuite) TestShouldFailAuthenticationBecausePreauthDenied() { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "deny", StatusMessage: "We're sorry, access is not allowed.", } provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "failure-icon") s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "There was an issue completing sign in process") } func (s *DuoPushWebDriverSuite) TestShouldSucceedAuthentication() { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer func() { cancel() s.collectScreenshot(ctx.Err(), s.Page) }() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }}, } // Setup Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyIsHome(s.T(), s.Context(ctx)) } func (s *DuoPushWebDriverSuite) TestShouldFailAuthentication() { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer func() { cancel() s.collectScreenshot(ctx.Err(), s.Page) }() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "auth", Devices: []duo.Device{{ Device: "12345ABCDEFGHIJ67890", DisplayName: "Test Device 1", Capabilities: []string{"auto", "push", "sms", "mobile_otp"}, }}, } // Setup Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Deny) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "failure-icon") } type DuoPushDefaultRedirectionSuite struct { *RodSuite } func NewDuoPushDefaultRedirectionSuite() *DuoPushDefaultRedirectionSuite { return &DuoPushDefaultRedirectionSuite{RodSuite: new(RodSuite)} } func (s *DuoPushDefaultRedirectionSuite) SetupSuite() { browser, err := StartRod() if err != nil { log.Fatal(err) } s.RodSession = browser } func (s *DuoPushDefaultRedirectionSuite) TearDownSuite() { err := s.RodSession.Stop() if err != nil { log.Fatal(err) } } func (s *DuoPushDefaultRedirectionSuite) SetupTest() { s.Page = s.doCreateTab(s.T(), HomeBaseURL) s.verifyIsHome(s.T(), s.Page) } func (s *DuoPushDefaultRedirectionSuite) TearDownTest() { s.collectCoverage(s.Page) s.MustClose() } func (s *DuoPushDefaultRedirectionSuite) TestUserIsRedirectedToDefaultURL() { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer func() { cancel() s.collectScreenshot(ctx.Err(), s.Page) }() var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "allow", StatusMessage: "Allowing unknown user", } // Setup Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "") s.doChangeMethod(s.T(), s.Context(ctx), "push-notification") s.verifyIsHome(s.T(), s.Page) // Clean up any Duo device already in DB. require.NoError(s.T(), provider.DeletePreferredDuoDevice(ctx, "john")) } type DuoPushSuite struct { suite.Suite } func NewDuoPushSuite() *DuoPushSuite { return &DuoPushSuite{} } func (s *DuoPushSuite) TestDuoPushWebDriverSuite() { suite.Run(s.T(), NewDuoPushWebDriverSuite()) } func (s *DuoPushSuite) TestDuoPushRedirectionURLSuite() { suite.Run(s.T(), NewDuoPushDefaultRedirectionSuite()) } func (s *DuoPushSuite) TestAvailableMethodsScenario() { suite.Run(s.T(), NewAvailableMethodsScenario([]string{ "TIME-BASED ONE-TIME PASSWORD", "PUSH NOTIFICATION", })) } func (s *DuoPushSuite) TestUserPreferencesScenario() { var PreAuthAPIResponse = duo.PreAuthResponse{ Result: "allow", StatusMessage: "Allowing unknown user", } ctx := context.Background() // Setup Duo device in DB. provider := storage.NewSQLiteProvider(&storageLocalTmpConfig) require.NoError(s.T(), provider.SavePreferredDuoDevice(ctx, models.DuoDevice{Username: "john", Device: "12345ABCDEFGHIJ67890", Method: "push"})) ConfigureDuoPreAuth(s.T(), PreAuthAPIResponse) ConfigureDuo(s.T(), Allow) suite.Run(s.T(), NewUserPreferencesScenario()) // Clean up any Duo device already in DB. require.NoError(s.T(), provider.DeletePreferredDuoDevice(ctx, "john")) } func TestDuoPushSuite(t *testing.T) { if testing.Short() { t.Skip("skipping suite test in short mode") } suite.Run(t, NewDuoPushSuite()) }