Using your authenticator app, please enter the one-time code that you see for APTrust.
+ + {{ if .errorMessage }} +{{ .errorMessage }}
+ {{ end }} + + +diff --git a/app/application.go b/app/application.go index 1c670477..93a5f0d4 100644 --- a/app/application.go +++ b/app/application.go @@ -263,6 +263,11 @@ func initRoutes(router *gin.Engine) { webRoutes.POST("/users/2fa_push", webui.UserTwoFactorPush) webRoutes.POST("/users/2fa_verify", webui.UserTwoFactorVerify) + // Generate and validate Time-based One-Time Passwords for MFA + webRoutes.GET("/users/generate_totp", webui.UserGenerateTOTP) + webRoutes.GET("/users/validate_totp", webui.UserValidateTOTPView) + webRoutes.POST("/users/validate_totp", webui.UserValidateTOTP) + // User forgot password webRoutes.GET("/users/forgot_password", webui.UserShowForgotPasswordForm) webRoutes.POST("/users/forgot_password", webui.UserSendForgotPasswordMessage) diff --git a/cfn/cfn-registry-cluster.yml b/cfn/cfn-registry-cluster.yml index de5837b5..d60d3932 100644 --- a/cfn/cfn-registry-cluster.yml +++ b/cfn/cfn-registry-cluster.yml @@ -200,7 +200,7 @@ Resources: - Condition: SUCCESS ContainerName: Nsqadmin_ResolvConf_InitContainer Essential: true - Image: 997427182289.dkr.ecr.us-east-1.amazonaws.com/docker-hub/aptrust/registry:bb2623b-master + Image: 997427182289.dkr.ecr.us-east-1.amazonaws.com/docker-hub/aptrust/registry:596d1ee-mfa-totp Secrets: - Name: APT_ENV ValueFrom: !Sub 'arn:aws:ssm:us-east-1:997427182289:parameter/${Envn}/REGISTRY/APT_ENV' diff --git a/constants/constants.go b/constants/constants.go index 3f31cfb3..5be1f6f8 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -84,6 +84,7 @@ const ( RoleInstUser = "institutional_user" RoleNone = "none" RoleSysAdmin = "admin" + SecondFactorTOTP = "Authenticator App" SecondFactorAuthy = "Authy" SecondFactorBackupCode = "Backup Code" SecondFactorSMS = "SMS" @@ -132,6 +133,8 @@ const ( TopicFixity = "fixity_check" TopicGlacierRestore = "restore_glacier" TopicObjectRestore = "restore_object" + TOTPSecretIssuer = "APTrust" + TwoFactorTOTP = "totp" TwoFactorAuthy = "onetouch" TwoFactorNone = "none" TwoFactorSMS = "sms" diff --git a/constants/permissions.go b/constants/permissions.go index ac97f99c..4876bc65 100644 --- a/constants/permissions.go +++ b/constants/permissions.go @@ -73,6 +73,7 @@ const ( UserDelete = "UserDelete" UserDeleteSelf = "UserDeleteSelf" UserGenerateBackupCodes = "UserGenerateBackupCodes" + UserGenerateTOTP = "UserGenerateTOTP" UserInit2FASetup = "UserInit2FASetup" UserRead = "UserRead" UserReadSelf = "UserReadSelf" @@ -86,6 +87,8 @@ const ( UserTwoFactorVerify = "UserTwoFactorVerify" UserUpdate = "UserUpdate" UserUpdateSelf = "UserUpdateSelf" + UserValidateTOTP = "UserValidateTOTP" + UserValidateTOTPView = "UserValidateTOTPView" WorkItemCreate = "WorkItemCreate" WorkItemDelete = "WorkItemDelete" WorkItemRead = "WorkItemRead" @@ -152,6 +155,7 @@ var Permissions = []Permission{ UserDelete, UserDeleteSelf, UserGenerateBackupCodes, + UserGenerateTOTP, UserInit2FASetup, UserRead, UserReadSelf, @@ -165,6 +169,8 @@ var Permissions = []Permission{ UserTwoFactorVerify, UserUpdate, UserUpdateSelf, + UserValidateTOTP, + UserValidateTOTPView, WorkItemCreate, WorkItemDelete, WorkItemRead, @@ -228,6 +234,7 @@ func initPermissions() { instUser[UserComplete2FASetup] = true instUser[UserConfirmPhone] = true instUser[UserGenerateBackupCodes] = true + instUser[UserGenerateTOTP] = true instUser[UserInit2FASetup] = true instUser[UserReadSelf] = true instUser[UserSignIn] = true @@ -238,6 +245,8 @@ func initPermissions() { instUser[UserTwoFactorPush] = true instUser[UserTwoFactorResend] = true instUser[UserTwoFactorVerify] = true + instUser[UserValidateTOTP] = true + instUser[UserValidateTOTPView] = true instUser[UserUpdateSelf] = true instUser[WorkItemRead] = true @@ -268,6 +277,7 @@ func initPermissions() { instAdmin[UserCreate] = true instAdmin[UserDelete] = true instAdmin[UserGenerateBackupCodes] = true + instAdmin[UserGenerateTOTP] = true instAdmin[UserInit2FASetup] = true instAdmin[UserReadSelf] = true instAdmin[UserRead] = true @@ -279,6 +289,8 @@ func initPermissions() { instAdmin[UserTwoFactorPush] = true instAdmin[UserTwoFactorResend] = true instAdmin[UserTwoFactorVerify] = true + instAdmin[UserValidateTOTP] = true + instAdmin[UserValidateTOTPView] = true instAdmin[UserUpdateSelf] = true instAdmin[UserUpdate] = true instAdmin[WorkItemRead] = true @@ -341,6 +353,7 @@ func initPermissions() { sysAdmin[UserDeleteSelf] = true sysAdmin[UserDelete] = true sysAdmin[UserGenerateBackupCodes] = true + sysAdmin[UserGenerateTOTP] = true sysAdmin[UserInit2FASetup] = true sysAdmin[UserReadSelf] = true sysAdmin[UserRead] = true @@ -352,6 +365,8 @@ func initPermissions() { sysAdmin[UserTwoFactorPush] = true sysAdmin[UserTwoFactorResend] = true sysAdmin[UserTwoFactorVerify] = true + sysAdmin[UserValidateTOTP] = true + sysAdmin[UserValidateTOTPView] = true sysAdmin[UserUpdateSelf] = true sysAdmin[UserUpdate] = true sysAdmin[WorkItemCreate] = true diff --git a/db/fixtures/users.csv b/db/fixtures/users.csv index 78247fdb..ebbad5ab 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,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,encrypted_auth_app_secret +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_add_auth_app_secret.sql b/db/migrations/013_add_auth_app_secret.sql new file mode 100644 index 00000000..b4da8cef --- /dev/null +++ b/db/migrations/013_add_auth_app_secret.sql @@ -0,0 +1,12 @@ +-- 013_add_auth_app_secret.sql +-- Adds a field to represent a secret from an authenticator app for use with MFA + +-- Note that we're starting the migration. +insert into schema_migrations ("version", started_at) values ('013_add_auth_app_secret', now()) +on conflict ("version") do update set started_at = now(); + +-- Add new encrypted_auth_app_secret to the users table +alter table users add column if not exists encrypted_auth_app_secret varchar null; + +-- Now mark the migration as completed. +update schema_migrations set finished_at = now() where "version" = '013_add_auth_app_secret'; diff --git a/db/schema.sql b/db/schema.sql index 0d324f5b..e3395439 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -501,6 +501,7 @@ CREATE TABLE public.users ( grace_period timestamp NULL, awaiting_second_factor bool NOT NULL DEFAULT false, "role" varchar(50) NOT NULL DEFAULT 'none'::character varying, + encrypted_auth_app_secret varchar NULL, CONSTRAINT users_pkey PRIMARY KEY (id) ); CREATE INDEX index_users_on_authy_id ON public.users USING btree (authy_id); diff --git a/forms/lists.go b/forms/lists.go index 428fe180..a40959ff 100644 --- a/forms/lists.go +++ b/forms/lists.go @@ -80,6 +80,7 @@ var StorageOptionList = []*ListOption{ var TwoFactorMethodList = []*ListOption{ {constants.TwoFactorNone, "None (Turn Off Two-Factor Authentication)", false}, + {constants.TwoFactorTOTP, "Authenticator App", 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..3d60b519 100644 --- a/forms/two_factor_setup_form.go +++ b/forms/two_factor_setup_form.go @@ -21,7 +21,7 @@ func (f *TwoFactorSetupForm) init() { f.Fields["AuthyStatus"] = &Field{ Name: "AuthyStatus", Label: "Preferred Method for Two-Factor Auth", - Placeholder: "", + Placeholder: "Authy", ErrMsg: "Please choose your preferred method.", Options: TwoFactorMethodList, Attrs: map[string]string{ diff --git a/go.mod b/go.mod index eb0fcd7f..686f3574 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -72,8 +73,10 @@ require ( 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/pquerna/otp v1.5.0 // indirect github.com/sanity-io/litter v1.5.5 // indirect github.com/sergi/go-diff v1.0.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/afero v1.1.2 // indirect github.com/spf13/cast v1.3.0 // indirect github.com/spf13/jwalterweatherman v1.0.0 // indirect diff --git a/go.sum b/go.sum index 76495f6b..38d7eed4 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/brianvoe/gofakeit/v6 v6.9.0 h1:UCGhPCKLiqBc910TKS7LcOGf74NozftibFCbGIS6GZQ= github.com/brianvoe/gofakeit/v6 v6.9.0/go.mod h1:palrJUk4Fyw38zIFB/uBZqsgzW5VsNllhHKKwAebzew= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= @@ -273,6 +275,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -297,6 +301,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/middleware/authenticate.go b/middleware/authenticate.go index 1b665163..1573a2a8 100644 --- a/middleware/authenticate.go +++ b/middleware/authenticate.go @@ -174,7 +174,7 @@ func SetDefaultHeaders(c *gin.Context) { // Set these security headers on all responses. c.Writer.Header().Set("X-XSS-Protection", "1") c.Writer.Header().Set("X-Content-Type-Options", "nosniff") - c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; font-src 'self' fonts.gstatic.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; script-src 'self' 'unsafe-inline'") + c.Writer.Header().Set("Content-Security-Policy", "default-src 'self'; font-src 'self' fonts.gstatic.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data:;") } func forceCompletionOfPasswordChange(c *gin.Context, currentUser *pgmodels.User) bool { @@ -198,6 +198,7 @@ func forceCompletionOfTwoFactorAuth(c *gin.Context, currentUser *pgmodels.User) p := c.FullPath() return currentUser.ResetPasswordToken == "" && currentUser.AwaitingSecondFactor && + !strings.HasPrefix(p, "/users/validate_totp") && !strings.HasPrefix(p, "/users/2fa_backup") && !strings.HasPrefix(p, "/users/2fa_choose") && !strings.HasPrefix(p, "/users/2fa_push") && diff --git a/middleware/authorization_map.go b/middleware/authorization_map.go index 77c4b867..799d4d2c 100644 --- a/middleware/authorization_map.go +++ b/middleware/authorization_map.go @@ -122,6 +122,7 @@ var AuthMap = map[string]AuthMetadata{ "UserDeleteSelf": {"User", constants.UserDeleteSelf, "Deactivate Your Account"}, "UserEdit": {"User", constants.UserUpdate, "Edit User"}, "UserGenerateBackupCodes": {"User", constants.UserGenerateBackupCodes, "Create Two-Factor Backup Codes"}, + "UserGenerateTOTP": {"User", constants.UserGenerateTOTP, "Authenticator App Setup"}, "UserGetAPIKey": {"User", constants.UserUpdateSelf, "Generate API Key"}, "UserIndex": {"User", constants.UserRead, "Users"}, "UserInit2FASetup": {"User", constants.UserInit2FASetup, "Start Two-Factor Setup"}, @@ -141,6 +142,8 @@ var AuthMap = map[string]AuthMetadata{ "UserUpdate": {"User", constants.UserUpdate, "Update User"}, "UserUpdateXHR": {"User", constants.UserUpdate, "Update User"}, "UserUpdateSelf": {"User", constants.UserUpdateSelf, "Update User"}, + "UserValidateTOTP": {"User", constants.UserValidateTOTP, "MFA Confirm TOTP Via Authenticator App"}, + "UserValidateTOTPView": {"User", constants.UserValidateTOTPView, "MFA Confirm TOTP Via Authenticator App Form"}, "WorkItemCreate": {"WorkItem", constants.WorkItemCreate, "Create Work Item"}, "WorkItemDelete": {"WorkItem", constants.WorkItemDelete, "Delete Work Item"}, "WorkItemEdit": {"WorkItem", constants.WorkItemUpdate, "Edit Work Item"}, diff --git a/notes.md b/notes.md index 20d34ab4..885ae219 100644 --- a/notes.md +++ b/notes.md @@ -102,11 +102,14 @@ Remember, depdenency hell and mountains of garbage code are only one npm package ### Login * Email/password login +* Two-factor authenticator app (recommended) * 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. +You can utilize any authenticator app that supports QR codes for two-factor authentication, including but not limited to Google Authenticator, Microsoft Authenticator, or Duo. + For two-factor auth, since we're already using Authy, try the [Go Client for Authy](https://github.com/dcu/go-authy). ### Edit @@ -345,6 +348,8 @@ The term "items" below refers to Intellectual Objects, Generic Files, Checksums, # Two Factor Authentication +The recommended method currently is to use an authenticator app on your device. You can use any authenticator app that supports QR codes, such as Google Authenticator, Microsoft Authenticator, or Duo. + 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. 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..90eddd6c 100644 --- a/pgmodels/user.go +++ b/pgmodels/user.go @@ -112,6 +112,12 @@ type User struct { // we're waiting for a user to enter a text/SMS OTP. EncryptedOTPSentAt time.Time `json:"-" form:"-" pg:"encrypted_otp_sent_at"` + // EncryptedAuthAppSecret is a secret value shared with the user's device. + // This value is not used for SMS OTP - that would be the EncryptedOTPSecret. + // Rather, this value is used with authenticator apps if the user has + // Authenticator App MFA enabled. + EncryptedAuthAppSecret string `json:"-" form:"-" pg:"encrypted_auth_app_secret"` + // ConsumedTimestep is a legacy field from Devise, which used it // for time-based one-time passwords. Not used. // TODO: Delete this. @@ -377,6 +383,12 @@ func (user *User) IsAuthyOneTouchUser() bool { return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorAuthy) } +// IsAuthenticatorAppUser returns true if the user has enabled an authenticator app +// for two-factor login. +func (user *User) IsAuthenticatorAppUser() bool { + return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorTOTP) +} + // IsTwoFactorUser returns true if this user has enabled and confirmed // two factor authentication. // @@ -401,7 +413,10 @@ func (user *User) TwoFactorMethod() string { if user.IsSMSUser() { return constants.TwoFactorSMS } - return constants.TwoFactorAuthy + if user.IsAuthyOneTouchUser() { + return constants.TwoFactorAuthy + } + return constants.TwoFactorTOTP } // CreateOTPToken creates a new one-time password token, typically diff --git a/views/users/choose_second_factor.html b/views/users/choose_second_factor.html index 073d8952..e2e9157a 100644 --- a/views/users/choose_second_factor.html +++ b/views/users/choose_second_factor.html @@ -25,6 +25,22 @@