From 5942e004126359e050b8f39dcda4fda775356097 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 7 Dec 2019 12:18:22 +0100 Subject: [PATCH] Introduce hasU2F and hasTOTP in user info. --- internal/authentication/const.go | 6 +- .../handlers/handler_2fa_available_methods.go | 2 +- internal/handlers/handler_2fa_preferences.go | 68 ------ .../handlers/handler_2fa_preferences_test.go | 129 ----------- internal/handlers/handler_user_info.go | 109 +++++++++ internal/handlers/handler_user_info_test.go | 213 ++++++++++++++++++ internal/handlers/types.go | 10 +- internal/mocks/mock_authelia_ctx.go | 8 + internal/server/server.go | 9 +- internal/storage/errors.go | 3 + internal/storage/provider.go | 2 +- internal/storage/sql_provider.go | 2 +- .../scenario_backend_protection_test.go | 4 +- internal/utils/strings.go | 10 + web/src/models/Methods.ts | 2 +- web/src/services/Api.ts | 3 +- web/src/services/UserPreferences.ts | 16 +- web/src/views/LoginPortal/LoginPortal.tsx | 2 +- .../SecondFactor/MethodSelectionDialog.tsx | 4 +- 19 files changed, 378 insertions(+), 224 deletions(-) delete mode 100644 internal/handlers/handler_2fa_preferences.go delete mode 100644 internal/handlers/handler_2fa_preferences_test.go create mode 100644 internal/handlers/handler_user_info.go create mode 100644 internal/handlers/handler_user_info_test.go create mode 100644 internal/utils/strings.go diff --git a/internal/authentication/const.go b/internal/authentication/const.go index 49dd6237..3a6b67ec 100644 --- a/internal/authentication/const.go +++ b/internal/authentication/const.go @@ -17,9 +17,9 @@ const ( TOTP = "totp" // U2F Method using U2F devices like Yubikeys U2F = "u2f" - // DuoPush Method using Duo application to receive push notifications. - DuoPush = "duo_push" + // Push Method using Duo application to receive push notifications. + Push = "mobile_push" ) // PossibleMethods is the set of all possible 2FA methods. -var PossibleMethods = []string{TOTP, U2F, DuoPush} +var PossibleMethods = []string{TOTP, U2F, Push} diff --git a/internal/handlers/handler_2fa_available_methods.go b/internal/handlers/handler_2fa_available_methods.go index 0bd67779..72ee789d 100644 --- a/internal/handlers/handler_2fa_available_methods.go +++ b/internal/handlers/handler_2fa_available_methods.go @@ -11,7 +11,7 @@ func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) { availableMethods := MethodList{authentication.TOTP, authentication.U2F} if ctx.Configuration.DuoAPI != nil { - availableMethods = append(availableMethods, authentication.DuoPush) + availableMethods = append(availableMethods, authentication.Push) } ctx.Logger.Debugf("Available methods are %s", availableMethods) diff --git a/internal/handlers/handler_2fa_preferences.go b/internal/handlers/handler_2fa_preferences.go deleted file mode 100644 index 56379381..00000000 --- a/internal/handlers/handler_2fa_preferences.go +++ /dev/null @@ -1,68 +0,0 @@ -package handlers - -import ( - "fmt" - - "github.com/clems4ever/authelia/internal/authentication" - "github.com/clems4ever/authelia/internal/middlewares" -) - -// SecondFactorPreferencesGet get the user preferences regarding 2FA. -func SecondFactorPreferencesGet(ctx *middlewares.AutheliaCtx) { - preferences := preferences{ - Method: "totp", - } - - userSession := ctx.GetSession() - method, err := ctx.Providers.StorageProvider.LoadPrefered2FAMethod(userSession.Username) - ctx.Logger.Debugf("Loaded prefered 2FA method of user %s is %s", userSession.Username, method) - - if err != nil { - ctx.Error(fmt.Errorf("Unable to load prefered 2FA method: %s", err), operationFailedMessage) - return - } - - if method != "" { - // Set the retrieved method. - preferences.Method = method - } - - ctx.SetJSONBody(preferences) -} - -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - -// SecondFactorPreferencesPost update the user preferences regarding 2FA. -func SecondFactorPreferencesPost(ctx *middlewares.AutheliaCtx) { - bodyJSON := preferences{} - - err := ctx.ParseBody(&bodyJSON) - if err != nil { - ctx.Error(err, operationFailedMessage) - return - } - - if !stringInSlice(bodyJSON.Method, authentication.PossibleMethods) { - ctx.Error(fmt.Errorf("Unknown method %s, it should be either u2f, totp or duo_push", bodyJSON.Method), operationFailedMessage) - return - } - - userSession := ctx.GetSession() - - ctx.Logger.Debugf("Save new prefered 2FA method of user %s to %s", userSession.Username, bodyJSON.Method) - err = ctx.Providers.StorageProvider.SavePrefered2FAMethod(userSession.Username, bodyJSON.Method) - - if err != nil { - ctx.Error(fmt.Errorf("Unable to save new prefered 2FA method: %s", err), operationFailedMessage) - return - } - - ctx.ReplyOK() -} diff --git a/internal/handlers/handler_2fa_preferences_test.go b/internal/handlers/handler_2fa_preferences_test.go deleted file mode 100644 index 6a62ab7d..00000000 --- a/internal/handlers/handler_2fa_preferences_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package handlers - -import ( - "fmt" - "testing" - - "github.com/clems4ever/authelia/internal/mocks" - - "github.com/golang/mock/gomock" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type SecondFactorPreferencesSuite struct { - suite.Suite - - mock *mocks.MockAutheliaCtx -} - -func (s *SecondFactorPreferencesSuite) SetupTest() { - s.mock = mocks.NewMockAutheliaCtx(s.T()) - // Set the intial user session. - userSession := s.mock.Ctx.GetSession() - userSession.Username = "john" - userSession.AuthenticationLevel = 1 - s.mock.Ctx.SaveSession(userSession) -} - -func (s *SecondFactorPreferencesSuite) TearDownTest() { - s.mock.Close() -} - -// GET - -func (s *SecondFactorPreferencesSuite) TestShouldGetPreferenceRetrievedFromStorage() { - s.mock.StorageProviderMock.EXPECT(). - LoadPrefered2FAMethod(gomock.Eq("john")). - Return("u2f", nil) - SecondFactorPreferencesGet(s.mock.Ctx) - - s.mock.Assert200OK(s.T(), preferences{Method: "u2f"}) -} - -func (s *SecondFactorPreferencesSuite) TestShouldGetDefaultPreferenceIfNotInDB() { - s.mock.StorageProviderMock.EXPECT(). - LoadPrefered2FAMethod(gomock.Eq("john")). - Return("", nil) - SecondFactorPreferencesGet(s.mock.Ctx) - - s.mock.Assert200OK(s.T(), preferences{Method: "totp"}) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenStorageFailsToLoad() { - s.mock.StorageProviderMock.EXPECT(). - LoadPrefered2FAMethod(gomock.Eq("john")). - Return("", fmt.Errorf("Failure")) - SecondFactorPreferencesGet(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unable to load prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -// POST - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenNoBodyProvided() { - SecondFactorPreferencesPost(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenMalformedBodyProvided() { - s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"")) - SecondFactorPreferencesPost(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenBadBodyProvided() { - s.mock.Ctx.Request.SetBody([]byte("{\"weird_key\":\"abc\"}")) - SecondFactorPreferencesPost(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unable to validate body: method: non zero value required", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenBadMethodProvided() { - s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"}")) - SecondFactorPreferencesPost(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unknown method abc, it should be either u2f, totp or duo_push", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenDatabaseFailsToSave() { - s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) - s.mock.StorageProviderMock.EXPECT(). - SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")). - Return(fmt.Errorf("Failure")) - - SecondFactorPreferencesPost(s.mock.Ctx) - - s.mock.Assert200KO(s.T(), "Operation failed.") - assert.Equal(s.T(), "Unable to save new prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message) - assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) -} - -func (s *SecondFactorPreferencesSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() { - s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) - s.mock.StorageProviderMock.EXPECT(). - SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")). - Return(nil) - - SecondFactorPreferencesPost(s.mock.Ctx) - - assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode()) -} - -func TestRunPreferencesSuite(t *testing.T) { - s := new(SecondFactorPreferencesSuite) - suite.Run(t, s) -} diff --git a/internal/handlers/handler_user_info.go b/internal/handlers/handler_user_info.go new file mode 100644 index 00000000..3acb55f4 --- /dev/null +++ b/internal/handlers/handler_user_info.go @@ -0,0 +1,109 @@ +package handlers + +import ( + "fmt" + "strings" + "sync" + + "github.com/clems4ever/authelia/internal/authentication" + "github.com/clems4ever/authelia/internal/middlewares" + "github.com/clems4ever/authelia/internal/storage" + "github.com/clems4ever/authelia/internal/utils" + "github.com/sirupsen/logrus" +) + +func loadInfo(username string, storageProvier storage.Provider, preferences *UserPreferences, logger *logrus.Entry) []error { + var wg sync.WaitGroup + wg.Add(3) + + errors := make([]error, 0) + go func() { + defer wg.Done() + method, err := storageProvier.LoadPrefered2FAMethod(username) + if err != nil { + errors = append(errors, err) + logger.Error(err) + return + } + if method == "" { + preferences.Method = authentication.PossibleMethods[0] + } else { + preferences.Method = method + } + }() + + go func() { + defer wg.Done() + _, _, err := storageProvier.LoadU2FDeviceHandle(username) + if err != nil { + if err == storage.ErrNoU2FDeviceHandle { + return + } + errors = append(errors, err) + logger.Error(err) + return + } + preferences.HasU2F = true + }() + + go func() { + defer wg.Done() + _, err := storageProvier.LoadTOTPSecret(username) + if err != nil { + if err == storage.ErrNoTOTPSecret { + return + } + errors = append(errors, err) + logger.Error(err) + return + } + preferences.HasTOTP = true + }() + + wg.Wait() + return errors +} + +// UserInfoGet get the info related to the user identitified by the session. +func UserInfoGet(ctx *middlewares.AutheliaCtx) { + userSession := ctx.GetSession() + + preferences := UserPreferences{} + errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &preferences, ctx.Logger) + + if len(errors) > 0 { + ctx.Error(fmt.Errorf("Unable to load user preferences"), operationFailedMessage) + return + } + ctx.SetJSONBody(preferences) +} + +type MethodBody struct { + Method string `json:"method" valid:"required"` +} + +// MethodPreferencePost update the user preferences regarding 2FA method. +func MethodPreferencePost(ctx *middlewares.AutheliaCtx) { + bodyJSON := MethodBody{} + err := ctx.ParseBody(&bodyJSON) + if err != nil { + ctx.Error(err, operationFailedMessage) + return + } + + if !utils.IsStringInSlice(bodyJSON.Method, authentication.PossibleMethods) { + ctx.Error(fmt.Errorf("Unknown method '%s', it should be one of %s", bodyJSON.Method, strings.Join(authentication.PossibleMethods, ", ")), operationFailedMessage) + return + } + + userSession := ctx.GetSession() + ctx.Logger.Debugf("Save new prefered 2FA method of user %s to %s", userSession.Username, bodyJSON.Method) + err = ctx.Providers.StorageProvider.SavePrefered2FAMethod(userSession.Username, bodyJSON.Method) + + if err != nil { + ctx.Error(fmt.Errorf("Unable to save new prefered 2FA method: %s", err), operationFailedMessage) + return + } + + ctx.ReplyOK() +} diff --git a/internal/handlers/handler_user_info_test.go b/internal/handlers/handler_user_info_test.go new file mode 100644 index 00000000..0a48c925 --- /dev/null +++ b/internal/handlers/handler_user_info_test.go @@ -0,0 +1,213 @@ +package handlers + +import ( + "fmt" + "testing" + + "github.com/clems4ever/authelia/internal/mocks" + + "github.com/golang/mock/gomock" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type FetchSuite struct { + suite.Suite + mock *mocks.MockAutheliaCtx +} + +func (s *FetchSuite) SetupTest() { + s.mock = mocks.NewMockAutheliaCtx(s.T()) + // Set the intial user session. + userSession := s.mock.Ctx.GetSession() + userSession.Username = "john" + userSession.AuthenticationLevel = 1 + s.mock.Ctx.SaveSession(userSession) +} + +func (s *FetchSuite) TearDownTest() { + s.mock.Close() +} + +func (s *FetchSuite) setPreferencesExpectations(preferences UserPreferences) { + s.mock.StorageProviderMock. + EXPECT(). + LoadPrefered2FAMethod(gomock.Eq("john")). + Return(preferences.Method, nil) + + var u2fData []byte + if preferences.HasU2F { + u2fData = []byte("abc") + } + + s.mock.StorageProviderMock. + EXPECT(). + LoadU2FDeviceHandle(gomock.Eq("john")). + Return(u2fData, u2fData, nil) + + var totpSecret string + if preferences.HasTOTP { + totpSecret = "secret" + } + + s.mock.StorageProviderMock. + EXPECT(). + LoadTOTPSecret(gomock.Eq("john")). + Return(totpSecret, nil) +} + +func (s *FetchSuite) TestMethodSetToU2F() { + table := []UserPreferences{ + UserPreferences{ + Method: "totp", + }, + UserPreferences{ + Method: "u2f", + HasU2F: true, + HasTOTP: true, + }, + UserPreferences{ + Method: "u2f", + HasU2F: true, + HasTOTP: false, + }, + } + + for _, expectedPreferences := range table { + s.setPreferencesExpectations(expectedPreferences) + UserInfoGet(s.mock.Ctx) + + actualPreferences := UserPreferences{} + s.mock.GetResponseData(s.T(), &actualPreferences) + + s.Run("expected method", func() { + s.Assert().Equal(expectedPreferences.Method, actualPreferences.Method) + }) + + s.Run("registered u2f", func() { + s.Assert().Equal(expectedPreferences.HasU2F, actualPreferences.HasU2F) + }) + + s.Run("registered totp", func() { + s.Assert().Equal(expectedPreferences.HasTOTP, actualPreferences.HasTOTP) + }) + } +} + +func (s *FetchSuite) TestShouldGetDefaultPreferenceIfNotInDB() { + s.mock.StorageProviderMock. + EXPECT(). + LoadPrefered2FAMethod(gomock.Eq("john")). + Return("", nil) + + s.mock.StorageProviderMock. + EXPECT(). + LoadU2FDeviceHandle(gomock.Eq("john")). + Return(nil, nil, nil) + + s.mock.StorageProviderMock. + EXPECT(). + LoadTOTPSecret(gomock.Eq("john")). + Return("", nil) + + UserInfoGet(s.mock.Ctx) + s.mock.Assert200OK(s.T(), UserPreferences{Method: "totp"}) +} + +func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() { + s.mock.StorageProviderMock.EXPECT(). + LoadPrefered2FAMethod(gomock.Eq("john")). + Return("", fmt.Errorf("Failure")) + UserInfoGet(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unable to load prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func TestFetchSuite(t *testing.T) { + suite.Run(t, &FetchSuite{}) +} + +type SaveSuite struct { + suite.Suite + mock *mocks.MockAutheliaCtx +} + +func (s *SaveSuite) SetupTest() { + s.mock = mocks.NewMockAutheliaCtx(s.T()) + // Set the intial user session. + userSession := s.mock.Ctx.GetSession() + userSession.Username = "john" + userSession.AuthenticationLevel = 1 + s.mock.Ctx.SaveSession(userSession) +} + +func (s *SaveSuite) TearDownTest() { + s.mock.Close() +} + +func (s *SaveSuite) TestShouldReturnError500WhenNoBodyProvided() { + s.mock.Ctx.Request.SetBody(nil) + MethodPreferencePost(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func (s *SaveSuite) TestShouldReturnError500WhenMalformedBodyProvided() { + s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"")) + MethodPreferencePost(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func (s *SaveSuite) TestShouldReturnError500WhenBadBodyProvided() { + s.mock.Ctx.Request.SetBody([]byte("{\"weird_key\":\"abc\"}")) + MethodPreferencePost(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unable to validate body: method: non zero value required", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func (s *SaveSuite) TestShouldReturnError500WhenBadMethodProvided() { + s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"}")) + MethodPreferencePost(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unknown method 'abc', it should be one of totp, u2f, mobile_push", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func (s *SaveSuite) TestShouldReturnError500WhenDatabaseFailsToSave() { + s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) + s.mock.StorageProviderMock.EXPECT(). + SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")). + Return(fmt.Errorf("Failure")) + + MethodPreferencePost(s.mock.Ctx) + + s.mock.Assert200KO(s.T(), "Operation failed.") + assert.Equal(s.T(), "Unable to save new prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message) + assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level) +} + +func (s *SaveSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() { + s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}")) + s.mock.StorageProviderMock.EXPECT(). + SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")). + Return(nil) + + MethodPreferencePost(s.mock.Ctx) + + assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode()) +} + +func TestSaveSuite(t *testing.T) { + suite.Run(t, &SaveSuite{}) +} diff --git a/internal/handlers/types.go b/internal/handlers/types.go index 51beec7e..9fb767d3 100644 --- a/internal/handlers/types.go +++ b/internal/handlers/types.go @@ -10,10 +10,16 @@ type MethodList = []string type authorizationMatching int -// preferences is the model of user second factor preferences -type preferences struct { +// UserInfo is the model of user second factor preferences +type UserPreferences struct { // The prefered 2FA method. Method string `json:"method" valid:"required"` + + // True if a security key has been registered + HasU2F bool `json:"has_u2f" valid:"required"` + + // True if a TOTP device has been registered + HasTOTP bool `json:"has_totp" valid:"required"` } // signTOTPRequestBody model of the request body received by TOTP authentication endpoint. diff --git a/internal/mocks/mock_authelia_ctx.go b/internal/mocks/mock_authelia_ctx.go index 870343e2..3c0f19df 100644 --- a/internal/mocks/mock_authelia_ctx.go +++ b/internal/mocks/mock_authelia_ctx.go @@ -9,6 +9,7 @@ import ( "github.com/clems4ever/authelia/internal/regulation" "github.com/clems4ever/authelia/internal/storage" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/clems4ever/authelia/internal/authorization" "github.com/clems4ever/authelia/internal/configuration/schema" @@ -143,3 +144,10 @@ func (m *MockAutheliaCtx) Assert200OK(t *testing.T, data interface{}) { assert.NoError(t, err) assert.Equal(t, string(b), string(m.Ctx.Response.Body())) } + +func (m *MockAutheliaCtx) GetResponseData(t *testing.T, data interface{}) { + okResponse := middlewares.OKResponse{} + okResponse.Data = data + err := json.Unmarshal(m.Ctx.Response.Body(), &okResponse) + require.NoError(t, err) +} diff --git a/internal/server/server.go b/internal/server/server.go index 601eac1b..05def5cc 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -49,10 +49,11 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi router.GET("/api/secondfactor/available", autheliaMiddleware( middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet))) - router.GET("/api/secondfactor/preferences", autheliaMiddleware( - middlewares.RequireFirstFactor(handlers.SecondFactorPreferencesGet))) - router.POST("/api/secondfactor/preferences", autheliaMiddleware( - middlewares.RequireFirstFactor(handlers.SecondFactorPreferencesPost))) + // Information about the user + router.GET("/api/user/info", autheliaMiddleware( + middlewares.RequireFirstFactor(handlers.UserInfoGet))) + router.POST("/api/user/info/2fa_method", autheliaMiddleware( + middlewares.RequireFirstFactor(handlers.MethodPreferencePost))) // TOTP related endpoints router.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware( diff --git a/internal/storage/errors.go b/internal/storage/errors.go index 40473dc2..1f8d4f9a 100644 --- a/internal/storage/errors.go +++ b/internal/storage/errors.go @@ -5,4 +5,7 @@ import "errors" var ( // ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB. ErrNoU2FDeviceHandle = errors.New("No U2F device handle found") + + // ErrNoTOTPSecret error thrown when no TOTP secret has been found in DB + ErrNoTOTPSecret = errors.New("No TOTP secret registered") ) diff --git a/internal/storage/provider.go b/internal/storage/provider.go index f2e468bc..02593974 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -20,7 +20,7 @@ type Provider interface { LoadTOTPSecret(username string) (string, error) SaveU2FDeviceHandle(username string, keyHandle []byte, publicKey []byte) error - LoadU2FDeviceHandle(username string) ([]byte, []byte, error) + LoadU2FDeviceHandle(username string) (keyHandle []byte, publicKey []byte, err error) AppendAuthenticationLog(attempt models.AuthenticationAttempt) error LoadLatestAuthenticationLogs(username string, fromDate time.Time) ([]models.AuthenticationAttempt, error) diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index a7bd3122..563cc11e 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -128,7 +128,7 @@ func (p *SQLProvider) LoadTOTPSecret(username string) (string, error) { var secret string if err := p.db.QueryRow(p.sqlGetTOTPSecretByUsername, username).Scan(&secret); err != nil { if err == sql.ErrNoRows { - return "", nil + return "", ErrNoTOTPSecret } return "", err } diff --git a/internal/suites/scenario_backend_protection_test.go b/internal/suites/scenario_backend_protection_test.go index 253caf22..62bc07b3 100644 --- a/internal/suites/scenario_backend_protection_test.go +++ b/internal/suites/scenario_backend_protection_test.go @@ -42,8 +42,8 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() { s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/sign_request", AutheliaBaseURL), 403) - s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403) - s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/preferences", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/user/info/2fa_method", AutheliaBaseURL), 403) + s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/user/info", AutheliaBaseURL), 403) s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403) diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 00000000..805d0288 --- /dev/null +++ b/internal/utils/strings.go @@ -0,0 +1,10 @@ +package utils + +func IsStringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} diff --git a/web/src/models/Methods.ts b/web/src/models/Methods.ts index 6f4e22b2..ab80212b 100644 --- a/web/src/models/Methods.ts +++ b/web/src/models/Methods.ts @@ -2,5 +2,5 @@ export enum SecondFactorMethod { TOTP = 1, U2F = 2, - Duo = 3 + MobilePush = 3 } diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts index f1dc4d5c..22850d08 100644 --- a/web/src/services/Api.ts +++ b/web/src/services/Api.ts @@ -21,7 +21,8 @@ export const ResetPasswordPath = "/api/reset-password" export const LogoutPath = "/api/logout"; export const StatePath = "/api/state"; -export const UserPreferencesPath = "/api/secondfactor/preferences"; +export const UserInfoPath = "/api/user/info"; +export const UserInfo2FAMethodPath = "/api/user/info/2fa_method"; export const Available2FAMethodsPath = "/api/secondfactor/available"; export interface ErrorResponse { diff --git a/web/src/services/UserPreferences.ts b/web/src/services/UserPreferences.ts index 3b4fa539..cd2b155c 100644 --- a/web/src/services/UserPreferences.ts +++ b/web/src/services/UserPreferences.ts @@ -1,9 +1,9 @@ import { Get, PostWithOptionalResponse } from "./Client"; -import { UserPreferencesPath } from "./Api"; +import { UserInfoPath, UserInfo2FAMethodPath } from "./Api"; import { SecondFactorMethod } from "../models/Methods"; import { UserPreferences } from "../models/UserPreferences"; -export type Method2FA = "u2f" | "totp" | "duo_push"; +export type Method2FA = "u2f" | "totp" | "mobile_push"; export interface UserPreferencesPayload { method: Method2FA; @@ -15,8 +15,8 @@ export function toEnum(method: Method2FA): SecondFactorMethod { return SecondFactorMethod.U2F; case "totp": return SecondFactorMethod.TOTP; - case "duo_push": - return SecondFactorMethod.Duo; + case "mobile_push": + return SecondFactorMethod.MobilePush; } } @@ -26,17 +26,17 @@ export function toString(method: SecondFactorMethod): Method2FA { return "u2f"; case SecondFactorMethod.TOTP: return "totp"; - case SecondFactorMethod.Duo: - return "duo_push"; + case SecondFactorMethod.MobilePush: + return "mobile_push"; } } export async function getUserPreferences(): Promise { - const res = await Get(UserPreferencesPath); + const res = await Get(UserInfoPath); return { method: toEnum(res.method) }; } export function setPrefered2FAMethod(method: SecondFactorMethod) { - return PostWithOptionalResponse(UserPreferencesPath, + return PostWithOptionalResponse(UserInfo2FAMethodPath, { method: toString(method) } as UserPreferencesPayload); } \ No newline at end of file diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index 8dbefeff..6a0a2e19 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -81,7 +81,7 @@ export default function () { console.log("redirect"); if (preferences.method === SecondFactorMethod.U2F) { redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`); - } else if (preferences.method === SecondFactorMethod.Duo) { + } else if (preferences.method === SecondFactorMethod.MobilePush) { redirect(`${SecondFactorPushRoute}${redirectionSuffix}`); } else { redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`); diff --git a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx index 4257204f..5c839e94 100644 --- a/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx +++ b/web/src/views/LoginPortal/SecondFactor/MethodSelectionDialog.tsx @@ -42,12 +42,12 @@ export default function (props: Props) { icon={} onClick={() => props.onClick(SecondFactorMethod.U2F)} /> : null} - {props.methods.has(SecondFactorMethod.Duo) + {props.methods.has(SecondFactorMethod.MobilePush) ? } - onClick={() => props.onClick(SecondFactorMethod.Duo)} /> + onClick={() => props.onClick(SecondFactorMethod.MobilePush)} /> : null}