diff --git a/network/authy.go b/network/authy.go deleted file mode 100644 index 394bfbce..00000000 --- a/network/authy.go +++ /dev/null @@ -1,116 +0,0 @@ -package network - -import ( - "errors" - stdlog "log" - "net/url" - "os" - "time" - - "github.com/brianvoe/gofakeit/v6" - "github.com/dcu/go-authy" - "github.com/rs/zerolog" -) - -// ErrAuthyDisabled means authy isn't enabled here. You can change that -// in the .env file. -var ErrAuthyDisabled = errors.New("authy is not enabled in this environment") - -var authyLoginMessage = "Log in to the APTrust registry." -var authyTimeout = (45 * time.Second) - -type AuthyClient struct { - client *authy.Authy - enabled bool - log zerolog.Logger -} - -type AuthyClientInterface interface { - AwaitOneTouch(string, string) (bool, error) - RegisterUser(string, int, string) (string, error) -} - -func NewAuthyClient(authyEnabled bool, authyAPIKey string, log zerolog.Logger) AuthyClientInterface { - // Authy library logs to Stderr by default. We want either our logger - // (which doesn't support the standard go logger interface) or Stdout - // because Docker gathers Stdout logs. - authy.Logger = stdlog.New(os.Stdout, "[authy] ", stdlog.LstdFlags) - return &AuthyClient{ - client: authy.NewAuthyAPI(authyAPIKey), - enabled: authyEnabled, - log: log, - } -} - -// AwaitOneTouch sends a OneTouch login request via Authy and awaits -// the user's response. Param authyID is the user's AuthyID. Param -// userEmail is used for logging. -// -// This is a blocking request that waits up to 45 seconds for a user -// to approve the one-touch push notification. -// -// If request is approved, this returns true. Otherwise, false. -func (ac *AuthyClient) AwaitOneTouch(userEmail, authyID string) (bool, error) { - if !ac.enabled { - return false, ErrAuthyDisabled - } - details := authy.Details{} - req, err := ac.client.SendApprovalRequest(authyID, authyLoginMessage, details, url.Values{}) - if err != nil { - ac.log.Error().Msgf("AuthyOneTouch error for %s: %v", userEmail, err) - return false, err - } - ac.log.Info().Msgf("AuthyOneTouch request id for %s: %s", userEmail, req.UUID) - status, err := ac.client.WaitForApprovalRequest(req.UUID, authyTimeout, url.Values{}) - if status == authy.OneTouchStatusApproved { - ac.log.Info().Msgf("AuthyOneTouch approved for %s (%s)", userEmail, req.UUID) - return true, nil - } else { - ac.log.Warn().Msgf("AuthyOneTouch %s for %s (%s)", status, userEmail, req.UUID) - } - return false, nil -} - -// RegisterUser registers a user with Authy for this app. Note that -// users need separate registrations for each environment (dev, demo, -// prod, etc.). -// -// On success, this returns the user's new AuthyID. The caller is -// responsible for attaching that ID to the user object and saving -// it to the database. -// -// Use user.CountryCodeAndPhone() to get country code and phone number, -// as these need to be separate. Do not pass user.PhoneNumber in format -// "+" because that won't work. -func (ac *AuthyClient) RegisterUser(userEmail string, countryCode int, phone string) (string, error) { - if !ac.enabled { - return "", ErrAuthyDisabled - } - authyUser, err := ac.client.RegisterUser(userEmail, countryCode, phone, url.Values{}) - if err != nil { - ac.log.Error().Msgf("Can't register user %s (%d %s) with Authy: %v", userEmail, countryCode, phone, err) - return "", err - } - return authyUser.ID, err -} - -// MockAuthyClient is used in testing. -type MockAuthyClient struct{} - -// NewMockAuthyClient returns a mock authy client for testing. -func NewMockAuthyClient() AuthyClientInterface { - return &MockAuthyClient{} -} - -// AwaitOneTouch for unit tests. Returns true unless param authyID == "fail". -func (m *MockAuthyClient) AwaitOneTouch(userEmail, authyID string) (bool, error) { - if authyID == "fail" { - return false, nil - } - return true, nil -} - -// RegisterUser for unit testing. Always succeeds. -func (m *MockAuthyClient) RegisterUser(userEmail string, countryCode int, phone string) (string, error) { - return gofakeit.FarmAnimal(), nil -} diff --git a/network/authy_test.go b/network/authy_test.go deleted file mode 100644 index ccddb853..00000000 --- a/network/authy_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package network_test - -import ( - "io/ioutil" - "testing" - - "github.com/APTrust/registry/network" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" -) - -func TestAuthyClient(t *testing.T) { - client := network.NewAuthyClient(false, "SecretKey", - zerolog.New(ioutil.Discard)) - assert.NotNil(t, client) - - // Test authy when disabled. - response, err := client.RegisterUser("homer@example.com", 1, "302-555-1212") - assert.Empty(t, response) - assert.Equal(t, network.ErrAuthyDisabled, err) - - ok, err := client.AwaitOneTouch("homer@example.com", "no-id") - assert.False(t, ok) - assert.Equal(t, network.ErrAuthyDisabled, err) - - // Test our mock authy client. This seems superfluous, but - // we want to make sure it works for tests in web/webui. - client = network.NewMockAuthyClient() - assert.NotNil(t, client) - - response, err = client.RegisterUser("homer@example.com", 1, "302-555-1212") - assert.NotEmpty(t, response) - assert.Nil(t, err) - - ok, err = client.AwaitOneTouch("homer@example.com", "no-id") - assert.True(t, ok) - assert.Nil(t, err) - - ok, err = client.AwaitOneTouch("homer@example.com", "fail") - assert.False(t, ok) - assert.Nil(t, err) -} diff --git a/notes.md b/notes.md index 20d34ab4..998d67a4 100644 --- a/notes.md +++ b/notes.md @@ -103,12 +103,9 @@ Remember, depdenency hell and mountains of garbage code are only one npm package * Email/password login * Two-factor text/sms -* Two-factor Authy To ensure users won't have to change their passwords when moving from the Rails app, implement the same password encryption scheme as Devise. The scheme is described [here](https://www.freecodecamp.org/news/how-does-devise-keep-your-passwords-safe-d367f6e816eb/), and the [Go bcrypt library](https://pkg.go.dev/golang.org/x/crypto/bcrypt) should be able to support it. -For two-factor auth, since we're already using Authy, try the [Go Client for Authy](https://github.com/dcu/go-authy). - ### Edit * edit details (phone, etc.) @@ -345,7 +342,7 @@ The term "items" below refers to Intellectual Objects, Generic Files, Checksums, # Two Factor Authentication -Current Pharos users who have enabled two-factor authentication receive one-time passwords through SMS or push notifications through Authy OneTouch. These methods were chosen after long discussion with depositors and we cannot change them without another long discussion. So for now, we're sticking with these two. +Current Pharos users who have enabled two-factor authentication receive one-time passwords through SMS. This method was chosen after long discussion with depositors and we cannot change this without another long discussion. So for now, we're sticking with these two. Notes on two-factor setup and workflow have grown large enoug to warrant their own document. See [Two Factor Notes](two_factor_notes.md). diff --git a/pgmodels/user.go b/pgmodels/user.go index 0917ced3..f0db7d3d 100644 --- a/pgmodels/user.go +++ b/pgmodels/user.go @@ -160,7 +160,7 @@ type User struct { // push, so they can login with one-touch. Anything else means SMS, // but call IsTwoFactorUser() to make sure they're actually require // two-factor auth before trying to text them. - AuthyStatus string `json:"authy_status" pg:"authy_status"` + MFAStatus string `json:"authy_status" pg:"authy_status"` // EmailVerified will be true once the system has verified that the // user's email address is correct. diff --git a/pgmodels/user_view.go b/pgmodels/user_view.go index 4a1b4e60..ed536605 100644 --- a/pgmodels/user_view.go +++ b/pgmodels/user_view.go @@ -37,7 +37,7 @@ type UserView struct { ConfirmedTwoFactor bool `json:"confirmed_two_factor" pg:"confirmed_two_factor"` AuthyID string `json:"-" pg:"authy_id"` LastSignInWithAuthy time.Time `json:"last_sign_in_with_authy" pg:"last_sign_in_with_authy"` - AuthyStatus string `json:"authy_status" pg:"authy_status"` + MFAStatus string `json:"authy_status" pg:"authy_status"` EmailVerified bool `json:"email_verified" pg:"email_verified"` InitialPasswordUpdated bool `json:"initial_password_updated" pg:"initial_password_updated"` ForcePasswordUpdate bool `json:"force_password_update" pg:"force_password_update"` diff --git a/views/users/backup_codes.html b/views/users/backup_codes.html index 2c08ff9b..c8f071cb 100644 --- a/views/users/backup_codes.html +++ b/views/users/backup_codes.html @@ -15,7 +15,7 @@

Backup Codes

Please copy the backup codes below. You can use these for two-factor login when you don't have access - to Authy or text messages. These backup codes supersede all previously-generated codes.

+ to text messages. These backup codes supersede all previously-generated codes.