Introduce hasU2F and hasTOTP in user info.
parent
778f069013
commit
5942e00412
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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{})
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package utils
|
||||
|
||||
func IsStringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -2,5 +2,5 @@
|
|||
export enum SecondFactorMethod {
|
||||
TOTP = 1,
|
||||
U2F = 2,
|
||||
Duo = 3
|
||||
MobilePush = 3
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<UserPreferences> {
|
||||
const res = await Get<UserPreferencesPayload>(UserPreferencesPath);
|
||||
const res = await Get<UserPreferencesPayload>(UserInfoPath);
|
||||
return { method: toEnum(res.method) };
|
||||
}
|
||||
|
||||
export function setPrefered2FAMethod(method: SecondFactorMethod) {
|
||||
return PostWithOptionalResponse(UserPreferencesPath,
|
||||
return PostWithOptionalResponse(UserInfo2FAMethodPath,
|
||||
{ method: toString(method) } as UserPreferencesPayload);
|
||||
}
|
|
@ -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}`);
|
||||
|
|
|
@ -42,12 +42,12 @@ export default function (props: Props) {
|
|||
icon={<FingerTouchIcon size={32} />}
|
||||
onClick={() => props.onClick(SecondFactorMethod.U2F)} />
|
||||
: null}
|
||||
{props.methods.has(SecondFactorMethod.Duo)
|
||||
{props.methods.has(SecondFactorMethod.MobilePush)
|
||||
? <MethodItem
|
||||
id="push-notification-option"
|
||||
method="Push Notification"
|
||||
icon={<PushNotificationIcon width={32} height={32} />}
|
||||
onClick={() => props.onClick(SecondFactorMethod.Duo)} />
|
||||
onClick={() => props.onClick(SecondFactorMethod.MobilePush)} />
|
||||
: null}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
|
Loading…
Reference in New Issue