From b3e2a91f563aa9cba2bf3ace467532fdbff16285 Mon Sep 17 00:00:00 2001 From: Xavier Michelon Date: Mon, 11 Nov 2024 08:21:44 +0100 Subject: [PATCH] feat(BRIDGE-205): add support for the IMAP AUTHENTICATE command. --- go.mod | 2 +- go.sum | 6 ++ tests/features/imap/auth.feature | 26 ++++++- tests/imap_test.go | 116 ++++++++++++++++++++++++++++--- tests/steps_test.go | 6 ++ 5 files changed, 144 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index aa8aac2a..9170f769 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.9 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.2.0 - github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e + github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.4.1-0.20240918100656-b4860af56d47 github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton diff --git a/go.sum b/go.sum index e936d2d0..56ade6a1 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,12 @@ github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032 h1:5bwI+mwF26c github.com/ProtonMail/gluon v0.17.1-0.20241014082854-9d93627be032/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e h1:+UfdKOkF9JEiH9VXWBo+/nlXNVSJcxtuf4+SJTrk9fw= github.com/ProtonMail/gluon v0.17.1-0.20241018144126-31e040c2417e/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087 h1:hqoJCo54y/4cO1w9ZfaqRMAvxdxJMRT0vc0ICbg8nVA= +github.com/ProtonMail/gluon v0.17.1-0.20241111071724-6536da14d087/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c h1:+klUNkIb8TMXxnE80PDJM5YV2gPfmyOal3hiofdGSAs= +github.com/ProtonMail/gluon v0.17.1-0.20241112080731-83106972325c/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce h1:lphIROziz1jya/E40KzWSDNm+tEyp86XkPk7qk1LgVY= +github.com/ProtonMail/gluon v0.17.1-0.20241112142609-f4ac4c4fbbce/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4= github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= diff --git a/tests/features/imap/auth.feature b/tests/features/imap/auth.feature index a4662e38..4ee4fc6e 100644 --- a/tests/features/imap/auth.feature +++ b/tests/features/imap/auth.feature @@ -13,26 +13,46 @@ Feature: A user can authenticate an IMAP client When user "[user:user]" connects IMAP client "1" Then IMAP client "1" can authenticate + Scenario: IMAP client can authenticate successfully using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" can authenticate using IMAP AUTHENTICATE + Scenario: IMAP client can authenticate successfully with different case When user "[user:user]" connects IMAP client "1" Then IMAP client "1" can authenticate with address "{toUpper:[user:user]@[domain]}" + Scenario: IMAP client can authenticate successfully with different case using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" can authenticate with address "{toUpper:[user:user]@[domain]}" using IMAP AUTHENTICATE + Scenario: IMAP client can authenticate successfully with secondary address Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" - Scenario: IMAP client can authenticate successfully - When user "[user:user]" connects IMAP client "1" - Then IMAP client "1" can authenticate + Scenario: IMAP client can authenticate successfully with secondary address using IMAP AUTHENTICATE + Given user "[user:user]" connects and authenticates IMAP client "1" with address "[alias:alias]@[domain]" using IMAP AUTHENTICATE Scenario: IMAP client cannot authenticate with bad username When user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate with incorrect username + Scenario: IMAP client cannot authenticate with bad username using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate with incorrect username using IMAP AUTHENTICATE + Scenario: IMAP client cannot authenticate with bad password When user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate with incorrect password + Scenario: IMAP client cannot authenticate with bad password using IMAP AUTHENTICATE + When user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate with incorrect password using IMAP AUTHENTICATE + Scenario: IMAP client cannot authenticate for disconnected user When user "[user:user]" logs out And user "[user:user]" connects IMAP client "1" Then IMAP client "1" cannot authenticate + + Scenario: IMAP client cannot authenticate using IMAP AUTHENTICATE for disconnected user + When user "[user:user]" logs out + And user "[user:user]" connects IMAP client "1" + Then IMAP client "1" cannot authenticate using IMAP AUTHENTICATE diff --git a/tests/imap_test.go b/tests/imap_test.go index 8245e6f7..cd21fb3b 100644 --- a/tests/imap_test.go +++ b/tests/imap_test.go @@ -36,10 +36,38 @@ import ( "github.com/emersion/go-imap" id "github.com/emersion/go-imap-id" "github.com/emersion/go-imap/client" + "github.com/emersion/go-sasl" "github.com/sirupsen/logrus" "golang.org/x/exp/slices" ) +type imapAuthMethod int + +const ( + imapLogin imapAuthMethod = iota + imapAuthenticate +) + +func (s *scenario) loginWithAuthMethod(client *client.Client, username, password string, authMethod imapAuthMethod) error { + switch authMethod { + case imapLogin: + return client.Login(username, password) + case imapAuthenticate: + supported, err := client.SupportAuth(sasl.Plain) + if err != nil { + return err + } + + if !supported { + return errors.New("server does not support AUTHENTICATE PLAIN") + } + + return client.Authenticate(sasl.NewPlainClient("", username, password)) + default: + return errors.New("unknown IMAP auth method") + } +} + func (s *scenario) userConnectsIMAPClient(username, clientID string) error { return s.t.newIMAPClient(s.t.getUserByName(username).getUserID(), clientID) } @@ -59,7 +87,17 @@ func (s *scenario) userConnectsAndAuthenticatesIMAPClientWithAddress(username, c userID, client := s.t.getIMAPClient(clientID) - return client.Login(address, s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) userConnectsAndAuthenticatesIMAPClientWithAddressUsingIMAPAuthenticate(username, clientID, address string) error { + if err := s.t.newIMAPClient(s.t.getUserByName(username).getUserID(), clientID); err != nil { + return err + } + + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(username, clientID, address string) error { @@ -69,7 +107,7 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(address, s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin); err == nil { return fmt.Errorf("expected error, got nil") } @@ -79,19 +117,51 @@ func (s *scenario) userConnectsAndCanNotAuthenticateIMAPClientWithAddress(userna func (s *scenario) imapClientCanAuthenticate(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - return client.Login(s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) imapClientCanAuthenticateUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) imapClientCanAuthenticateWithAddress(clientID string, address string) error { userID, client := s.t.getIMAPClient(clientID) - return client.Login(address, s.t.getUserByID(userID).getBridgePass()) + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin) +} + +func (s *scenario) imapClientCanAuthenticateWithAddressUsingIMAPAuthenticate(clientID string, address string) error { + userID, client := s.t.getIMAPClient(clientID) + + return s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapAuthenticate) } func (s *scenario) imapClientCannotAuthenticate(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0], s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0], + s.t.getUserByID(userID).getBridgePass(), + imapLogin, + ); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0], + s.t.getUserByID(userID).getBridgePass(), + imapAuthenticate, + ); err == nil { return fmt.Errorf("expected error, got nil") } @@ -101,7 +171,7 @@ func (s *scenario) imapClientCannotAuthenticate(clientID string) error { func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(address, s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod(client, address, s.t.getUserByID(userID).getBridgePass(), imapLogin); err == nil { return fmt.Errorf("expected error, got nil") } @@ -111,7 +181,27 @@ func (s *scenario) imapClientCannotAuthenticateWithAddress(clientID, address str func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID string) error { userID, client := s.t.getIMAPClient(clientID) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0]+"bad", s.t.getUserByID(userID).getBridgePass()); err == nil { + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0]+"bad", + s.t.getUserByID(userID).getBridgePass(), + imapLogin, + ); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsernameUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + + if err := s.loginWithAuthMethod( + client, + s.t.getUserByID(userID).getEmails()[0]+"bad", + s.t.getUserByID(userID).getBridgePass(), + imapAuthenticate, + ); err == nil { return fmt.Errorf("expected error, got nil") } @@ -121,7 +211,17 @@ func (s *scenario) imapClientCannotAuthenticateWithIncorrectUsername(clientID st func (s *scenario) imapClientCannotAuthenticateWithIncorrectPassword(clientID string) error { userID, client := s.t.getIMAPClient(clientID) badPass := base64.StdEncoding.EncodeToString([]byte("bad_password")) - if err := client.Login(s.t.getUserByID(userID).getEmails()[0], badPass); err == nil { + if err := s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], badPass, imapLogin); err == nil { + return fmt.Errorf("expected error, got nil") + } + + return nil +} + +func (s *scenario) imapClientCannotAuthenticateWithIncorrectPasswordUsingIMAPAuthenticate(clientID string) error { + userID, client := s.t.getIMAPClient(clientID) + badPass := base64.StdEncoding.EncodeToString([]byte("bad_password")) + if err := s.loginWithAuthMethod(client, s.t.getUserByID(userID).getEmails()[0], badPass, imapAuthenticate); err == nil { return fmt.Errorf("expected error, got nil") } diff --git a/tests/steps_test.go b/tests/steps_test.go index 470a2a0e..12c8cfff 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -129,13 +129,19 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)" on port (\d+)$`, s.userConnectsIMAPClientOnPort) ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClient) ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)" with address "([^"]*)"$`, s.userConnectsAndAuthenticatesIMAPClientWithAddress) + ctx.Step(`^user "([^"]*)" connects and authenticates IMAP client "([^"]*)" with address "([^"]*)" using IMAP AUTHENTICATE$`, s.userConnectsAndAuthenticatesIMAPClientWithAddressUsingIMAPAuthenticate) ctx.Step(`^user "([^"]*)" connects and can not authenticate IMAP client "([^"]*)" with address "([^"]*)"$`, s.userConnectsAndCanNotAuthenticateIMAPClientWithAddress) ctx.Step(`^IMAP client "([^"]*)" can authenticate$`, s.imapClientCanAuthenticate) + ctx.Step(`^IMAP client "([^"]*)" can authenticate using IMAP AUTHENTICATE$`, s.imapClientCanAuthenticateUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" can authenticate with address "([^"]*)"$`, s.imapClientCanAuthenticateWithAddress) + ctx.Step(`^IMAP client "([^"]*)" can authenticate with address "([^"]*)" using IMAP AUTHENTICATE$`, s.imapClientCanAuthenticateWithAddressUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate$`, s.imapClientCannotAuthenticate) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with address "([^"]*)"$`, s.imapClientCannotAuthenticateWithAddress) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username$`, s.imapClientCannotAuthenticateWithIncorrectUsername) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect username using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateWithIncorrectUsernameUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect password$`, s.imapClientCannotAuthenticateWithIncorrectPassword) + ctx.Step(`^IMAP client "([^"]*)" cannot authenticate with incorrect password using IMAP AUTHENTICATE$`, s.imapClientCannotAuthenticateWithIncorrectPasswordUsingIMAPAuthenticate) ctx.Step(`^IMAP client "([^"]*)" closes$`, s.imapClientCloses) ctx.Step(`^IMAP client "([^"]*)" announces its ID with name "([^"]*)" and version "([^"]*)"$`, s.imapClientAnnouncesItsIDWithNameAndVersion) ctx.Step(`^IMAP client "([^"]*)" creates "([^"]*)"$`, s.imapClientCreatesMailbox)