diff --git a/.env.dev b/.env.dev index 6ae5db34..1e1f6c50 100644 --- a/.env.dev +++ b/.env.dev @@ -96,14 +96,6 @@ SNS_ENDPOINT="" AWS_SNS_USER="" AWS_SNS_PWD="" - -# If you have valid Authy keys, set this to true. -# You'll need to set the following env variable: -# -# AUTHY_API_KEY= -# -ENABLE_TWO_FACTOR_AUTHY=false - # How long should one-time passwords sent via text/SMS last? # Use Go duration format. Examples: # "60s" = 60 seconds diff --git a/.env.docker b/.env.docker index b525cc6c..77e9661a 100644 --- a/.env.docker +++ b/.env.docker @@ -13,7 +13,6 @@ # See Parameter Store for a description of each variable. # -------------------------------------------------------------------------- # -# AUTHY_API_KEY # AWS_SES_PWD # AWS_SES_USER # AWS_SNS_USER @@ -33,7 +32,6 @@ # EMAIL_ENABLED # EMAIL_FROM_ADDRESS # EMAIL_SERVICE_TYPE -# ENABLE_TWO_FACTOR_AUTHY # ENABLE_TWO_FACTOR_SMS # FLASH_COOKIE_NAME # HTTPS_COOKIES diff --git a/.env.integration b/.env.integration index ebde1c7e..67f1d784 100644 --- a/.env.integration +++ b/.env.integration @@ -96,15 +96,6 @@ SNS_ENDPOINT="" AWS_SNS_USER="" AWS_SNS_PWD="" - - -# If you have valid Authy keys, set this to true. -# You'll need to set the following env variable: -# -# AUTHY_API_KEY= -# -ENABLE_TWO_FACTOR_AUTHY=false - # How long should one-time passwords sent via text/SMS last? # Use Go duration format. Examples: # "60s" = 60 seconds diff --git a/.env.test b/.env.test index 7df947b0..8ac1d88e 100644 --- a/.env.test +++ b/.env.test @@ -97,14 +97,6 @@ SNS_ENDPOINT="" AWS_SNS_USER="" AWS_SNS_PWD="" - -# If you have valid Authy keys, set this to true. -# You'll need to set the following env variable: -# -# AUTHY_API_KEY= -# -ENABLE_TWO_FACTOR_AUTHY=false - # How long should one-time passwords sent via text/SMS last? # Use Go duration format. Examples: # "60s" = 60 seconds diff --git a/.env.travis b/.env.travis index ff5feacb..b9664eaa 100644 --- a/.env.travis +++ b/.env.travis @@ -96,14 +96,6 @@ SNS_ENDPOINT="" AWS_SNS_USER="" AWS_SNS_PWD="" - -# If you have valid Authy keys, set this to true. -# You'll need to set the following env variable: -# -# AUTHY_API_KEY= -# -ENABLE_TWO_FACTOR_AUTHY=false - # How long should one-time passwords sent via text/SMS last? # Use Go duration format. Examples: # "60s" = 60 seconds diff --git a/Dockerfile b/Dockerfile index 2c903d51..97dd889a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,9 +50,6 @@ ENV AWS_REGION=us-east-1 ENV ENABLE_TWO_FACTOR_SMS=true -ENV ENABLE_TWO_FACTOR_AUTHY=true -ENV AUTHY_API_KEY= - ENV OTP_EXPIRATION="15m" ENV EMAIL_ENABLED=false diff --git a/Dockerfile.multi b/Dockerfile.multi index 03c5370d..44d8cf0a 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -62,9 +62,6 @@ ENV AWS_REGION=us-east-1 ENV ENABLE_TWO_FACTOR_SMS=true -ENV ENABLE_TWO_FACTOR_AUTHY=true -ENV AUTHY_API_KEY= - ENV OTP_EXPIRATION="15m" ENV EMAIL_ENABLED=false diff --git a/README.md b/README.md index 9ca9a758..844e8e56 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Right-click each of the NSQ binaries, then hold the Shift key and select Open fr ## Requirements for Two Factor Authentication -As a developer, you generally won't need to send Authy or SMS messages for two-factor authentication. If you're running this in a demo or production environment, you will. +As a developer, you generally won't need to send SMS messages for two-factor authentication. If you're running this in a demo or production environment, you will. The AWS SNS library, which sends two-factor auth codes via text/SMS, requires the following config files: @@ -166,17 +166,16 @@ You can call `testutil.InitHTTPTests()` before any HTTP tests to ensure HTTP tes # External Services -If you want to send two-factor OTP codes through SMS, or two-factor Authy push notifications, or password reset emails, you'll need to enable these in the .env (or .env.test) file. Set the following: +If you want to send two-factor OTP codes through SMS, or password reset emails, you'll need to enable these in the .env (or .env.test) file. Set the following: ``` ENABLE_TWO_FACTOR_SMS=true -ENABLE_TWO_FACTOR_AUTHY=true EMAIL_ENABLED=true ``` If these services are causing problems on your dev machine, you can turn them off by changing the settings to `false`. You can still log in with two-factor authentication when these services are turned off locally. The registry will print the OTP to the terminal console during development. You can cut and paste the OTP code from there. -You'll have to manually set a test user's phone number and/or Authy ID to send OTP messages successfully. +You'll have to manually set a test user's phone number to send OTP messages successfully. ## AWS Environment Variables @@ -188,9 +187,6 @@ AWS_SECRET_ACCESS_KEY= AWS_REGION="us-east-1" ``` -## Authy Environment Variables - -To use Authy for OTP, set the `AUTHY_API_KEY` environment variable. # Notes on Routes @@ -225,7 +221,7 @@ Routes excepted from auth checks include: The "forgot password" and "password reset" pages both use a secure token in the query string to identify the user. -The authentication middleware also hijacks all requests in cases where a user must complete registration activities. For example, if a user needs to reset their password, confirm an Authy account, or provide the second factor for multi-factor auth, the middleware will not let them past the reset/confirmation/token page until they've provided the required info. +The authentication middleware also hijacks all requests in cases where a user must complete registration activities. For example, if a user needs to reset their password or provide the second factor for multi-factor auth, the middleware will not let them past the reset/confirmation/token page until they've provided the required info. # Notes on JSON Serialization diff --git a/app/application.go b/app/application.go index 1c670477..dc4d108f 100644 --- a/app/application.go +++ b/app/application.go @@ -260,7 +260,6 @@ func initRoutes(router *gin.Engine) { webRoutes.GET("/users/2fa_backup", webui.UserTwoFactorBackup) webRoutes.GET("/users/2fa_choose", webui.UserTwoFactorChoose) webRoutes.POST("/users/2fa_sms", webui.UserTwoFactorGenerateSMS) - webRoutes.POST("/users/2fa_push", webui.UserTwoFactorPush) webRoutes.POST("/users/2fa_verify", webui.UserTwoFactorVerify) // User forgot password diff --git a/cfn/cfn-registry-cluster.tmpl b/cfn/cfn-registry-cluster.tmpl index 24e5d921..1f1d734a 100644 --- a/cfn/cfn-registry-cluster.tmpl +++ b/cfn/cfn-registry-cluster.tmpl @@ -206,8 +206,6 @@ Resources: ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/APT_ENV' - Name: LOG_DIR ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/LOG_DIR' - - Name: AUTHY_API_KEY - ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/AUTHY_API_KEY' - Name: AWS_ACCESS_KEY_ID ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/AWS_ACCESS_KEY_ID' - Name: AWS_REGION @@ -242,8 +240,6 @@ Resources: ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/EMAIL_ENABLED' - Name: EMAIL_FROM_ADDRESS ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/EMAIL_FROM_ADDRESS' - - Name: ENABLE_TWO_FACTOR_AUTHY - ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/ENABLE_TWO_FACTOR_AUTHY' - Name: ENABLE_TWO_FACTOR_SMS ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/ENABLE_TWO_FACTOR_SMS' - Name: FLASH_COOKIE_NAME diff --git a/cfn/cfn-registry-cluster.yml b/cfn/cfn-registry-cluster.yml index 0971a845..9e7f43d6 100644 --- a/cfn/cfn-registry-cluster.yml +++ b/cfn/cfn-registry-cluster.yml @@ -210,8 +210,6 @@ Resources: ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/APT_ENV' - Name: LOG_DIR ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/LOG_DIR' - - Name: AUTHY_API_KEY - ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/AUTHY_API_KEY' - Name: AWS_ACCESS_KEY_ID ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/AWS_ACCESS_KEY_ID' - Name: AWS_REGION @@ -246,8 +244,6 @@ Resources: ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/EMAIL_ENABLED' - Name: EMAIL_FROM_ADDRESS ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/EMAIL_FROM_ADDRESS' - - Name: ENABLE_TWO_FACTOR_AUTHY - ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/ENABLE_TWO_FACTOR_AUTHY' - Name: ENABLE_TWO_FACTOR_SMS ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/ENABLE_TWO_FACTOR_SMS' - Name: FLASH_COOKIE_NAME diff --git a/common/config.go b/common/config.go index a60908f7..46c00b94 100644 --- a/common/config.go +++ b/common/config.go @@ -61,14 +61,12 @@ type LoggingConfig struct { } // TwoFactorConfig contains info for sending push messages -// through Authy and SMS text messages through AWS SNS. +// through SMS text messages through AWS SNS. // If SNSEndpoint is empty, we'll use the default public // SNS endpoint for the specified region. If non-empty, // we'll use the explicit SNSEndpoint. Should be non-empty // if we're on a private subnet without a NAT gateway. type TwoFactorConfig struct { - AuthyEnabled bool - AuthyAPIKey string AWSRegion string SMSEnabled bool OTPExpiration time.Duration @@ -276,8 +274,6 @@ func loadConfig() *Config { EmailServiceType: emailServiceType, MaintenanceMode: v.GetBool("MAINTENANCE_MODE"), TwoFactor: &TwoFactorConfig{ - AuthyAPIKey: v.GetString("AUTHY_API_KEY"), - AuthyEnabled: v.GetBool("ENABLE_TWO_FACTOR_AUTHY"), AWSRegion: v.GetString("AWS_REGION"), SMSEnabled: v.GetBool("ENABLE_TWO_FACTOR_SMS"), OTPExpiration: v.GetDuration("OTP_EXPIRATION"), @@ -385,7 +381,6 @@ func (config *Config) ToJSON() (string, error) { copyOfConfig.Email.SesUser = maskString(config.Email.SesUser) copyOfConfig.Email.SesPassword = maskString(config.Email.SesPassword) copyOfConfig.Redis.Password = maskString(config.Redis.Password) - copyOfConfig.TwoFactor.AuthyAPIKey = maskString(config.TwoFactor.AuthyAPIKey) copyOfConfig.TwoFactor.SNSUser = maskString(config.TwoFactor.SNSUser) copyOfConfig.TwoFactor.SNSPassword = maskString(config.TwoFactor.SNSPassword) diff --git a/common/config_test.go b/common/config_test.go index 150b8191..c338b75d 100644 --- a/common/config_test.go +++ b/common/config_test.go @@ -61,7 +61,6 @@ func TestNewConfig(t *testing.T) { config.Email.SesUser = "mask-me-sesuser" config.Email.SesPassword = "mask-me-pwd-ses" config.Redis.Password = "mask-me-pwd-redis" - config.TwoFactor.AuthyAPIKey = "mask-me-authy" config.TwoFactor.SNSUser = "user-1234" config.TwoFactor.SNSPassword = "password-5678" config.TwoFactor.SNSEndpoint = "sns.example.com:886" @@ -177,8 +176,6 @@ var expectedConfigJson = `{ }, "NsqUrl": "http://localhost:4151", "TwoFactor": { - "AuthyEnabled": false, - "AuthyAPIKey": "****thy", "AWSRegion": "", "SMSEnabled": false, "OTPExpiration": 900000000000, diff --git a/common/context.go b/common/context.go index cee1c0c7..2213a986 100644 --- a/common/context.go +++ b/common/context.go @@ -24,10 +24,6 @@ type APTContext struct { // Log is our logger. Log zerolog.Logger - // AuthyClient sends push notifications to users who have enabled - // two-factor auth via push. - AuthyClient network.AuthyClientInterface - // NSQClient lets registry queue work items for preservation services // and lets us view NSQ admin stats. NSQClient *network.NSQClient @@ -85,7 +81,6 @@ func Context() *APTContext { Config: config, DB: db, Log: zlogger, - AuthyClient: network.NewAuthyClient(config.TwoFactor.AuthyEnabled, config.TwoFactor.AuthyAPIKey, zlogger), NSQClient: network.NewNSQClient(config.NsqUrl, zlogger), SESClient: network.NewSESClient(config.Email.Enabled, config.TwoFactor.AWSRegion, config.Email.SesEndpoint, config.Email.SesUser, config.Email.SesPassword, config.Email.FromAddress, zlogger), SNSClient: network.NewSNSClient(config.TwoFactor.SMSEnabled, config.TwoFactor.AWSRegion, config.TwoFactor.SNSEndpoint, config.TwoFactor.SNSUser, config.TwoFactor.SNSPassword, zlogger), diff --git a/common/context_test.go b/common/context_test.go index ebf28072..ed6d2252 100644 --- a/common/context_test.go +++ b/common/context_test.go @@ -14,7 +14,6 @@ func TestAPTContext(t *testing.T) { require.NotNil(t, ctx.Config) require.NotNil(t, ctx.DB) require.NotNil(t, ctx.Log) - require.NotNil(t, ctx.AuthyClient) require.NotNil(t, ctx.NSQClient) assert.NotEmpty(t, ctx.NSQClient.URL) require.NotNil(t, ctx.SNSClient) diff --git a/common/errors.go b/common/errors.go index dec3e6bd..d7d54c60 100644 --- a/common/errors.go +++ b/common/errors.go @@ -87,14 +87,6 @@ var ErrCrossOriginReferer = errors.New("csrf error: cross origin request forbidd // does not meet minimum requirements. var ErrPasswordReqs = errors.New("password does not meet minimum requirements") -// ErrAlreadyHasAuthyID occurs when we try to register a user with Authy -// and they already have an Authy ID. -var ErrAlreadyHasAuthyID = errors.New("user is already registered with authy") - -// ErrNoAuthyID occurs when a user requests two-factor login via Authy -// but does not have an Authy ID. -var ErrNoAuthyID = errors.New("user does not have an authy id") - // ErrWrongAPI occurs when a non-admin user tries to access the admin API. // While the member and admin APIs share some common handlers, and members // do technically have access to a number of read-only operations in both diff --git a/constants/constants.go b/constants/constants.go index 3f31cfb3..a12cdfd8 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -84,7 +84,6 @@ const ( RoleInstUser = "institutional_user" RoleNone = "none" RoleSysAdmin = "admin" - SecondFactorAuthy = "Authy" SecondFactorBackupCode = "Backup Code" SecondFactorSMS = "SMS" StageAvailableInS3 = "Available in S3" @@ -132,7 +131,6 @@ const ( TopicFixity = "fixity_check" TopicGlacierRestore = "restore_glacier" TopicObjectRestore = "restore_object" - TwoFactorAuthy = "onetouch" TwoFactorNone = "none" TwoFactorSMS = "sms" ) @@ -230,7 +228,6 @@ var Roles = []string{ } var SecondFactorTypes = []string{ - SecondFactorAuthy, SecondFactorBackupCode, SecondFactorSMS, } diff --git a/db/fixtures/users.csv b/db/fixtures/users.csv index 78247fdb..7541a3b6 100644 --- a/db/fixtures/users.csv +++ b/db/fixtures/users.csv @@ -1,10 +1,10 @@ -id,name,email,phone_number,created_at,updated_at,encrypted_password,reset_password_token,reset_password_sent_at,remember_created_at,sign_in_count,current_sign_in_at,last_sign_in_at,current_sign_in_ip,last_sign_in_ip,institution_id,encrypted_api_secret_key,password_changed_at,encrypted_otp_secret,encrypted_otp_secret_iv,encrypted_otp_secret_salt,encrypted_otp_sent_at,consumed_timestep,otp_required_for_login,deactivated_at,enabled_two_factor,confirmed_two_factor,otp_backup_codes,authy_id,last_sign_in_with_authy,authy_status,email_verified,initial_password_updated,force_password_update,account_confirmed,grace_period,awaiting_second_factor,role -4,Inactive User,inactive@inst1.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,1/15/21 13:49,FALSE,FALSE,"{code1,code2,code3}",,,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,none -5,Inst Two Admin,admin@inst2.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,3,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,,,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_admin -7,Inst Two User,user@inst2.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,3,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,,,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_user -2,Inst One Admin,admin@inst1.edu,14345551212,1/12/21 17:14,9/10/21 14:22,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,institutional_admin -3,Inst One User,user@inst1.edu,14345551212,1/12/21 17:14,9/10/21 14:22,$2a$10$raEJqJ7eRcEwWmeoiJ2vxenR8dqVXCI1SU9zcgkrxeS.6/haWGi4K,,,,1,9/10/21 14:22,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,institutional_user -1,APTrust System,system@aptrust.org,14345551212,1/12/21 17:14,9/10/21 14:24,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,1,9/10/21 14:24,,127.0.0.1,,1,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,admin -6,Two Factor SMS User,sms_user@example.com,12125551212,9/10/21 14:25,9/10/21 14:25,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,,,,,,,,TRUE,,,,,,,,,,TRUE,TRUE,11/9/21 5:00,FALSE,institutional_user -8,Test.edu Admin,admin@test.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,4,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,,,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_admin -9,Test.edu User,user@test.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,4,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,,,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_user \ No newline at end of file +id,name,email,phone_number,created_at,updated_at,encrypted_password,reset_password_token,reset_password_sent_at,remember_created_at,sign_in_count,current_sign_in_at,last_sign_in_at,current_sign_in_ip,last_sign_in_ip,institution_id,encrypted_api_secret_key,password_changed_at,encrypted_otp_secret,encrypted_otp_secret_iv,encrypted_otp_secret_salt,encrypted_otp_sent_at,consumed_timestep,otp_required_for_login,deactivated_at,enabled_two_factor,confirmed_two_factor,otp_backup_codes,email_verified,initial_password_updated,force_password_update,account_confirmed,grace_period,awaiting_second_factor,role,mfa_status +4,Inactive User,inactive@inst1.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,1/15/21 13:49,FALSE,FALSE,"{code1,code2,code3}",TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,none, +5,Inst Two Admin,admin@inst2.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,3,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_admin, +7,Inst Two User,user@inst2.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,3,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_user, +2,Inst One Admin,admin@inst1.edu,14345551212,1/12/21 17:14,9/10/21 14:22,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,institutional_admin, +3,Inst One User,user@inst1.edu,14345551212,1/12/21 17:14,9/10/21 14:22,$2a$10$raEJqJ7eRcEwWmeoiJ2vxenR8dqVXCI1SU9zcgkrxeS.6/haWGi4K,,,,1,9/10/21 14:22,,,,2,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,institutional_user, +1,APTrust System,system@aptrust.org,14345551212,1/12/21 17:14,9/10/21 14:24,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,1,9/10/21 14:24,,127.0.0.1,,1,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,,,,TRUE,TRUE,,TRUE,12/31/99 23:59,FALSE,admin, +6,Two Factor SMS User,sms_user@example.com,12125551212,9/10/21 14:25,9/10/21 14:25,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,2,,,,,,,,TRUE,,,,,,,TRUE,TRUE,11/9/21 5:00,FALSE,institutional_user, +8,Test.edu Admin,admin@test.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,4,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_admin, +9,Test.edu User,user@test.edu,14345551212,1/12/21 17:14,1/12/21 17:14,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,0,,,,,4,$2a$10$7aoot2KFFqikpTYVEbErYOxZijCHDPvqT4OMoFwdmsYBE9SK2PibC,,,,,,,,,FALSE,FALSE,,TRUE,TRUE,FALSE,TRUE,12/31/99 23:59,FALSE,institutional_user, \ No newline at end of file diff --git a/db/migrations/013_remove_authy.sql b/db/migrations/013_remove_authy.sql new file mode 100644 index 00000000..d0bf445f --- /dev/null +++ b/db/migrations/013_remove_authy.sql @@ -0,0 +1,66 @@ +-- 013_posix_metadata.sql +-- +-- This migration removes Authy-related columns and indices from the database. + +-- Note that we're starting the migration. +insert into schema_migrations ("version", started_at) values ('013_remove_authy', now()) +on conflict ("version") do update set started_at = now(); + +-- Add a generic MFA status column to replace the Authy one. +alter table public.users add column mfa_status varchar NULL; + +-- Drop and recreate the user view +drop view public.users_view; +CREATE VIEW public.users_view +AS SELECT u.id, + u.name, + u.email, + u.phone_number, + u.created_at, + u.updated_at, + u.reset_password_sent_at, + u.remember_created_at, + u.sign_in_count, + u.current_sign_in_at, + u.last_sign_in_at, + u.current_sign_in_ip, + u.last_sign_in_ip, + u.institution_id, + u.password_changed_at, + u.consumed_timestep, + u.otp_required_for_login, + u.deactivated_at, + u.enabled_two_factor, + u.confirmed_two_factor, + u.mfa_status, + u.email_verified, + u.initial_password_updated, + u.force_password_update, + u.account_confirmed, + u.grace_period, + u.role, + i.name AS institution_name, + i.identifier AS institution_identifier, + i.state AS institution_state, + i.type AS institution_type, + i.member_institution_id, + i2.name AS member_institution_name, + i2.identifier AS member_institution_identifier, + i2.state AS member_institution_state, + i.otp_enabled, + i.receiving_bucket, + i.restore_bucket + FROM users u + LEFT JOIN institutions i ON u.institution_id = i.id + LEFT JOIN institutions i2 ON i.member_institution_id = i2.id; + +-- Drop Authy columns. +alter table public.users drop column if exists authy_id; +alter table public.users drop column if exists last_sign_in_with_authy; +alter table public.users drop column if exists authy_status; + +-- Drop Authy index. +drop index if exists index_users_on_authy_id; + +-- Now mark the migration as completed. +update schema_migrations set finished_at = now() where "version" = '013_remove_authy'; diff --git a/env-dev b/env-dev index 81cf0581..2371928e 100644 --- a/env-dev +++ b/env-dev @@ -62,13 +62,6 @@ NSQ_URL='http://localhost:4151' # ENABLE_TWO_FACTOR_SMS=true -# If you have valid Authy keys, set this to true. -# You'll need to set the following env variable: -# -# AUTHY_API_KEY= -# -ENABLE_TWO_FACTOR_AUTHY=true - # How long should one-time passwords sent via text/SMS last? # Use Go duration format. Examples: # "60s" = 60 seconds diff --git a/forms/lists.go b/forms/lists.go index 428fe180..b736c498 100644 --- a/forms/lists.go +++ b/forms/lists.go @@ -80,7 +80,6 @@ var StorageOptionList = []*ListOption{ var TwoFactorMethodList = []*ListOption{ {constants.TwoFactorNone, "None (Turn Off Two-Factor Authentication)", false}, - {constants.TwoFactorAuthy, "Authy OneTouch", false}, {constants.TwoFactorSMS, "Text Message", false}, } diff --git a/forms/two_factor_setup_form.go b/forms/two_factor_setup_form.go index bd9f8f42..1d53696a 100644 --- a/forms/two_factor_setup_form.go +++ b/forms/two_factor_setup_form.go @@ -18,8 +18,8 @@ func NewTwoFactorSetupForm(user *pgmodels.User) *TwoFactorSetupForm { } func (f *TwoFactorSetupForm) init() { - f.Fields["AuthyStatus"] = &Field{ - Name: "AuthyStatus", + f.Fields["MFAStatus"] = &Field{ + Name: "MFAStatus", Label: "Preferred Method for Two-Factor Auth", Placeholder: "", ErrMsg: "Please choose your preferred method.", @@ -43,5 +43,5 @@ func (f *TwoFactorSetupForm) init() { func (f *TwoFactorSetupForm) SetValues() { user := f.Model.(*pgmodels.User) f.Fields["PhoneNumber"].Value = user.PhoneNumber - f.Fields["AuthyStatus"].Value = user.AuthyStatus + f.Fields["MFAStatus"].Value = user.MFAStatus } diff --git a/forms/two_factor_setup_form_test.go b/forms/two_factor_setup_form_test.go index 196919fb..6e5a04ea 100644 --- a/forms/two_factor_setup_form_test.go +++ b/forms/two_factor_setup_form_test.go @@ -13,14 +13,14 @@ import ( func TestTwoFactorSetupForm(t *testing.T) { user := &pgmodels.User{} user.ID = 99999 - user.AuthyStatus = constants.TwoFactorSMS + user.MFAStatus = constants.TwoFactorSMS user.PhoneNumber = "+1 505-867-5309" form := forms.NewTwoFactorSetupForm(user) require.NotNil(t, form) assert.Equal(t, 2, len(form.Fields)) - assert.NotNil(t, form.Fields["AuthyStatus"]) - assert.Equal(t, user.AuthyStatus, form.Fields["AuthyStatus"].Value) + assert.NotNil(t, form.Fields["MFAStatus"]) + assert.Equal(t, user.MFAStatus, form.Fields["MFAStatus"].Value) assert.NotNil(t, form.Fields["PhoneNumber"]) assert.Equal(t, user.PhoneNumber, form.Fields["PhoneNumber"].Value) diff --git a/go.mod b/go.mod index 38d91f35..ed557d2d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/aws/aws-sdk-go v1.49.23 github.com/brianvoe/gofakeit/v6 v6.9.0 - github.com/dcu/go-authy v1.0.1 github.com/gavv/httpexpect/v2 v2.14.0 github.com/gin-contrib/logger v0.0.2 github.com/gin-gonic/gin v1.9.1 @@ -43,10 +42,14 @@ require ( github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect +<<<<<<< HEAD + github.com/golang/snappy v0.0.3 // indirect +======= github.com/gojektech/heimdall v5.0.2+incompatible // indirect github.com/gojektech/valkyrie v0.0.0-20190210220504-8f62c1e7ba45 // indirect github.com/golang/protobuf v1.5.0 // indirect github.com/golang/snappy v0.0.4 // indirect +>>>>>>> qa github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -68,7 +71,6 @@ require ( github.com/nsqio/go-nsq v1.1.0 // indirect github.com/pelletier/go-toml v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pkg/errors v0.8.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect diff --git a/go.sum b/go.sum index b9ab7545..b258ada7 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,6 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dcu/go-authy v1.0.1 h1:9LtF0otuGKQOD0AzyAUKzT+etvzGZxpvf4x20/xiW1Y= -github.com/dcu/go-authy v1.0.1/go.mod h1:SJ8cuAYQ9c9ZGGIGNk5gyH87/Q0uJXzhb1DR8MNnn98= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fasthttp/websocket v1.4.3-rc.6 h1:omHqsl8j+KXpmzRjF8bmzOSYJ8GnS0E3efi1wYT+niY= @@ -109,10 +107,6 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gojektech/heimdall v5.0.2+incompatible h1:mfGLnHNTKN7b1OMTO4ZvL3oT2P13kqTTV7owK7BZDck= -github.com/gojektech/heimdall v5.0.2+incompatible/go.mod h1:8hRIZ3+Kz0r3GAFI9QrUuvZht8ypg5Rs8schCXioLOo= -github.com/gojektech/valkyrie v0.0.0-20190210220504-8f62c1e7ba45 h1:MO2DsGCZz8phRhLnpFvHEQgTH521sVN/6F2GZTbNO3Q= -github.com/gojektech/valkyrie v0.0.0-20190210220504-8f62c1e7ba45/go.mod h1:tDYRk1s5Pms6XJjj5m2PxAzmQvaDU8GqDf1u6x7yxKw= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -277,7 +271,6 @@ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/json_types.go b/pgmodels/json_types.go index d8d63c03..c047c584 100644 --- a/pgmodels/json_types.go +++ b/pgmodels/json_types.go @@ -13,8 +13,7 @@ import ( // also serialize info about the users who created, approved, or // cancelled the request. In this context, we do not want to serialize // info about the user that we would typically serialize in a user management -// endpoint. This would include things like whether the user has registered -// for Authy two-factor authentication, where the last logged in from, etc. +// endpoint. This would include things like where the last logged in from, etc. // // All we really need to serialize in such a case is the user's name, id, and // email address. diff --git a/pgmodels/user.go b/pgmodels/user.go index 0917ced3..f343049f 100644 --- a/pgmodels/user.go +++ b/pgmodels/user.go @@ -119,11 +119,11 @@ type User struct { // OTPRequiredForLogin indicates whether, as a matter of policy, the // user must use some form of OTP to log in. If true, the user should - // be allowed to log in only with Authy one-touch, six-digit SMS - // code, or backup code. + // be allowed to log in only with six-digit SMS + // code or backup code. // // Use IsTwoFactorUser() to actually determine whether we should - // force this user to use Authy, SMS, or backup code to get it. + // force this user to use SMS or backup code to get it. // This is messy because it has to mirror the legacy Rails implementation // for now. OTPRequiredForLogin bool `json:"otp_required_for_login" pg:"otp_required_for_login"` @@ -139,28 +139,18 @@ type User struct { EnabledTwoFactor bool `json:"enabled_two_factor" form:"-" pg:"enabled_two_factor"` // ConfirmedTwoFactor indicates that the system has confirmed this - // user's phone number (for SMS) and Authy account (for Authy 2FA). + // user's phone number (for SMS) ConfirmedTwoFactor bool `json:"confirmed_two_factor" form:"-" pg:"confirmed_two_factor"` // OTPBackupCodes is a list of backup codes the user can use to - // log if they can't get in via SMS or Authy. + // log if they can't get in via SMS. OTPBackupCodes []string `json:"-" form:"-" pg:"otp_backup_codes,array"` - // AuthyID is the user's Authy ID. We need this to send them push - // messages to complete one-touch sign-in. This will be empty for - // those who don't use Authy. - AuthyID string `json:"-" form:"-" pg:"authy_id"` - - // LastSignInWithAuthy is the timestamp of this user's last - // successful sign-in with Authy. - LastSignInWithAuthy time.Time `json:"last_sign_in_with_authy" form:"-" pg:"last_sign_in_with_authy"` - - // AuthyStatus indicates how the user wants to authenticate with - // Authy. If it's constants.TwoFactorAuthy, we should send them a - // push, so they can login with one-touch. Anything else means SMS, + // MFAStatus indicates how the user wants to authenticate with + // MFA. The current only option is 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:"mfa_status" pg:"mfa_status"` // EmailVerified will be true once the system has verified that the // user's email address is correct. @@ -177,7 +167,7 @@ type User struct { ForcePasswordUpdate bool `json:"force_password_update" form:"-" pg:"force_password_update"` // GracePeriod is a legacy field from the old Rails app. It held the - // date by which a user MUST complete either Authy or SMS two-factor + // date by which a user MUST complete SMS two-factor // setup. This feature was universally despised, and we won't be // using it unless someone complains. For now, consider this as // unused. @@ -185,7 +175,7 @@ type User struct { // AwaitingSecondFactor indicates that the user has logged in with // email and password, but has not yet completed the second login - // step (Authy, SMS OTP, or backup code). This flag is only set on + // step (SMS OTP or backup code). This flag is only set on // users going through the two-factor process. Middleware checks it // to prevent partially logged-in users from accessing any pages other // than those required to complete the two-factor login process. @@ -368,13 +358,7 @@ func (user *User) IsAdmin() bool { // IsSMSUser returns true if this user has enabled two-factor authentication // with SMS/text message. func (user *User) IsSMSUser() bool { - return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorSMS || user.AuthyStatus == "") -} - -// IsAuthyOneTouchUser returns true if the user has enabled Authy one touch -// for two-factor login. -func (user *User) IsAuthyOneTouchUser() bool { - return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorAuthy) + return user.IsTwoFactorUser() } // IsTwoFactorUser returns true if this user has enabled and confirmed @@ -390,18 +374,14 @@ func (user *User) IsTwoFactorUser() bool { // // constants.TwoFactorNone if the user does not use two-factor auth. // -// constants.TwoFactorAuthy if the user uses two-factor auth via Authy. -// // constants.TwoFactorSMS if the user receives two-factor OTP code via // text/SMS func (user *User) TwoFactorMethod() string { if !user.IsTwoFactorUser() { return constants.TwoFactorNone } - if user.IsSMSUser() { - return constants.TwoFactorSMS - } - return constants.TwoFactorAuthy + // If using 2FA, must be SMS + return constants.TwoFactorSMS } // CreateOTPToken creates a new one-time password token, typically diff --git a/pgmodels/user_test.go b/pgmodels/user_test.go index 73214c59..72604e1f 100644 --- a/pgmodels/user_test.go +++ b/pgmodels/user_test.go @@ -345,17 +345,14 @@ func TestIsSMSUser(t *testing.T) { user := &pgmodels.User{ EnabledTwoFactor: true, ConfirmedTwoFactor: true, - AuthyStatus: constants.TwoFactorSMS, + MFAStatus: constants.TwoFactorSMS, } assert.True(t, user.IsSMSUser()) - user.AuthyStatus = "" + user.MFAStatus = "" assert.True(t, user.IsSMSUser()) - user.AuthyStatus = constants.TwoFactorAuthy - assert.False(t, user.IsSMSUser()) - - user.AuthyStatus = constants.TwoFactorSMS + user.MFAStatus = constants.TwoFactorSMS user.ConfirmedTwoFactor = false assert.False(t, user.IsSMSUser()) @@ -363,28 +360,6 @@ func TestIsSMSUser(t *testing.T) { assert.False(t, user.IsSMSUser()) } -func TestIsAuthyOneTouchUser(t *testing.T) { - user := &pgmodels.User{ - EnabledTwoFactor: true, - ConfirmedTwoFactor: true, - AuthyStatus: constants.TwoFactorAuthy, - } - assert.True(t, user.IsAuthyOneTouchUser()) - - user.AuthyStatus = "" - assert.False(t, user.IsAuthyOneTouchUser()) - - user.AuthyStatus = constants.TwoFactorSMS - assert.False(t, user.IsAuthyOneTouchUser()) - - user.AuthyStatus = constants.TwoFactorAuthy - user.ConfirmedTwoFactor = false - assert.False(t, user.IsAuthyOneTouchUser()) - - user.EnabledTwoFactor = false - assert.False(t, user.IsAuthyOneTouchUser()) -} - func TestIsTwoFactorUser(t *testing.T) { user := &pgmodels.User{ EnabledTwoFactor: true, @@ -403,18 +378,15 @@ func TestTwoFactorMethod(t *testing.T) { user := &pgmodels.User{ EnabledTwoFactor: true, ConfirmedTwoFactor: true, - AuthyStatus: constants.TwoFactorSMS, + MFAStatus: constants.TwoFactorSMS, } assert.Equal(t, constants.TwoFactorSMS, user.TwoFactorMethod()) // Holdover from old system. A confirmed two-factor user - // with empty authy status is an SMS user. - user.AuthyStatus = "" + // with empty MFA status is an SMS user. + user.MFAStatus = "" assert.Equal(t, constants.TwoFactorSMS, user.TwoFactorMethod()) - user.AuthyStatus = constants.TwoFactorAuthy - assert.Equal(t, constants.TwoFactorAuthy, user.TwoFactorMethod()) - user.EnabledTwoFactor = false assert.Equal(t, constants.TwoFactorNone, user.TwoFactorMethod()) diff --git a/pgmodels/user_view.go b/pgmodels/user_view.go index 4a1b4e60..a90a9375 100644 --- a/pgmodels/user_view.go +++ b/pgmodels/user_view.go @@ -35,9 +35,7 @@ type UserView struct { DeactivatedAt time.Time `json:"deactivated_at" pg:"deactivated_at"` EnabledTwoFactor bool `json:"enabled_two_factor" pg:"enabled_two_factor"` 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:"mfa_status" pg:"mfa_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/two_factor_notes.md b/two_factor_notes.md index 1ffae525..9a1350ea 100644 --- a/two_factor_notes.md +++ b/two_factor_notes.md @@ -3,13 +3,11 @@ ## Task List - [x] Allow user to enable/disable 2FA -- [x] Allow user to choose SMS or Authy as primary 2FA method +- [x] Allow user to choose SMS as primary 2FA method - [x] Verify user phone number for SMS -- [x] Register Authy account for new users - [x] Choose 2FA method page (post-login) - [x] Generate and send OTP via SMS - [x] Verify/Reject SMS code -- [x] Send Authy OneTouch - [x] Accept/Reject OneTouch response - [x] Generate Backup Codes - [x] Verify/Reject Backup Code @@ -36,24 +34,19 @@ 4. Show screen to choose second factor method -5. If user chooses Authy: - * Send Authy one-touch and wait for reply - * If confirmed, redirect to dashboard - * Else sign out & return to sign-in screen - -6. If user chooses SMS: +5. If user chooses SMS: * Generate SMS & store encrypted in DB w/expiration time * Send SMS code * Go to 8 -7. If user chooses backup code +6. If user chooses backup code * Show page with backup code entry box -8. Show page with code entry box +7. Show page with code entry box * Include hidden field with auth type: SMS or backup code * If SMS, page should include a link to generate another code (timeout, not received, etc) -9. Verify code +8. Verify code * If invalid, re-display page with error message. * On fourth invalid attempt, lock account for 5 minutes. * If valid, clear restricted access flag & redirect to dashboard. @@ -68,7 +61,6 @@ Restrict users awaiting two-factor token verification to the following pages: | /users/2fa\_backup | Enter backup code | | /users/2fa\_choose | Choose second factor method | | /users/2fa\_sms | Send SMS code, show enter auth token form | -| /users/2fa\_push | Push Authy one-touch and wait for response | | /users/2fa\_resend | Resend SMS code | | /users/2fa\_verify | Verify SMS/Backup code | @@ -86,14 +78,8 @@ If visiting unauthorized page, redirect to Choose Second factor page. This is in * SMS * If chosen, have user enter and confirm phone number -* Authy - * If chosen, have user enter phone and then we'll register them with Authy * None (2fa disabled) -### Authy Registration - -It looks like Pharos uses Authy OneTouch, not time-based codes. So we shouldn't need to show the user a QR code on Authy setup. Just register the user with Authy's [RegisterUser](https://github.com/dcu/go-authy#creating-users) call. - ### Phone Confirmation Send an initial OTP via SMS and wait for the user to enter it. @@ -104,8 +90,6 @@ Generate and display five backup codes. These should be random strings of 10 cha ## Misc Notes -If we ever do have to generate QR codes for Authy, here's a [basic overview of the registration process](https://socketloop.com/tutorials/golang-how-to-implement-two-factor-authentication) and here's some [sample code for generating QR codes](https://www.socketloop.com/tutorials/golang-generate-qr-codes-for-google-authenticator-app-and-fix-cannot-interpret-qr-code-error). - This gist provides a simple example of [how to send a text message](https://gist.github.com/BizarroDavid/40f644de19a93039de5e67439de704b4). The main SNS library, in all its horror, is [documented here](https://docs.aws.amazon.com/sdk-for-go/api/service/sns/). For logging purposes, see the documentation on [Publish](https://docs.aws.amazon.com/sdk-for-go/api/service/sns/#SNS.Publish) and [PublishOutput](https://docs.aws.amazon.com/sdk-for-go/api/service/sns/#PublishOutput). It would be nice if we could link the message ID of the publish output to the CloudTrail log entry that describes the message's disposition. that would simplify the process of tracing problematic texts. diff --git a/views/users/authy_registration_complete.html b/views/users/authy_registration_complete.html deleted file mode 100644 index 7dec1ae2..00000000 --- a/views/users/authy_registration_complete.html +++ /dev/null @@ -1,27 +0,0 @@ -{{ define "users/authy_registration_complete.html" }} - - -{{ if not .showAsModal }} - {{ template "shared/_header.html" .}} -{{ end }} - - - - -{{ if not .showAsModal }} - {{ template "shared/_footer.html" .}} -{{ end }} - -{{ end }} 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.