Introduce hasU2F and hasTOTP in user info.

pull/482/head
Clement Michaud 2019-12-07 12:18:22 +01:00 committed by Clément Michaud
parent 778f069013
commit 5942e00412
19 changed files with 378 additions and 224 deletions

View File

@ -17,9 +17,9 @@ const (
TOTP = "totp" TOTP = "totp"
// U2F Method using U2F devices like Yubikeys // U2F Method using U2F devices like Yubikeys
U2F = "u2f" U2F = "u2f"
// DuoPush Method using Duo application to receive push notifications. // Push Method using Duo application to receive push notifications.
DuoPush = "duo_push" Push = "mobile_push"
) )
// PossibleMethods is the set of all possible 2FA methods. // PossibleMethods is the set of all possible 2FA methods.
var PossibleMethods = []string{TOTP, U2F, DuoPush} var PossibleMethods = []string{TOTP, U2F, Push}

View File

@ -11,7 +11,7 @@ func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) {
availableMethods := MethodList{authentication.TOTP, authentication.U2F} availableMethods := MethodList{authentication.TOTP, authentication.U2F}
if ctx.Configuration.DuoAPI != nil { if ctx.Configuration.DuoAPI != nil {
availableMethods = append(availableMethods, authentication.DuoPush) availableMethods = append(availableMethods, authentication.Push)
} }
ctx.Logger.Debugf("Available methods are %s", availableMethods) ctx.Logger.Debugf("Available methods are %s", availableMethods)

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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{})
}

View File

@ -10,10 +10,16 @@ type MethodList = []string
type authorizationMatching int type authorizationMatching int
// preferences is the model of user second factor preferences // UserInfo is the model of user second factor preferences
type preferences struct { type UserPreferences struct {
// The prefered 2FA method. // The prefered 2FA method.
Method string `json:"method" valid:"required"` 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. // signTOTPRequestBody model of the request body received by TOTP authentication endpoint.

View File

@ -9,6 +9,7 @@ import (
"github.com/clems4ever/authelia/internal/regulation" "github.com/clems4ever/authelia/internal/regulation"
"github.com/clems4ever/authelia/internal/storage" "github.com/clems4ever/authelia/internal/storage"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/clems4ever/authelia/internal/authorization" "github.com/clems4ever/authelia/internal/authorization"
"github.com/clems4ever/authelia/internal/configuration/schema" "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.NoError(t, err)
assert.Equal(t, string(b), string(m.Ctx.Response.Body())) 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)
}

View File

@ -49,10 +49,11 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.GET("/api/secondfactor/available", autheliaMiddleware( router.GET("/api/secondfactor/available", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet))) middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet)))
router.GET("/api/secondfactor/preferences", autheliaMiddleware( // Information about the user
middlewares.RequireFirstFactor(handlers.SecondFactorPreferencesGet))) router.GET("/api/user/info", autheliaMiddleware(
router.POST("/api/secondfactor/preferences", autheliaMiddleware( middlewares.RequireFirstFactor(handlers.UserInfoGet)))
middlewares.RequireFirstFactor(handlers.SecondFactorPreferencesPost))) router.POST("/api/user/info/2fa_method", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.MethodPreferencePost)))
// TOTP related endpoints // TOTP related endpoints
router.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware( router.POST("/api/secondfactor/totp/identity/start", autheliaMiddleware(

View File

@ -5,4 +5,7 @@ import "errors"
var ( var (
// ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB. // ErrNoU2FDeviceHandle error thrown when no U2F device handle has been found in DB.
ErrNoU2FDeviceHandle = errors.New("No U2F device handle found") 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")
) )

View File

@ -20,7 +20,7 @@ type Provider interface {
LoadTOTPSecret(username string) (string, error) LoadTOTPSecret(username string) (string, error)
SaveU2FDeviceHandle(username string, keyHandle []byte, publicKey []byte) 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 AppendAuthenticationLog(attempt models.AuthenticationAttempt) error
LoadLatestAuthenticationLogs(username string, fromDate time.Time) ([]models.AuthenticationAttempt, error) LoadLatestAuthenticationLogs(username string, fromDate time.Time) ([]models.AuthenticationAttempt, error)

View File

@ -128,7 +128,7 @@ func (p *SQLProvider) LoadTOTPSecret(username string) (string, error) {
var secret string var secret string
if err := p.db.QueryRow(p.sqlGetTOTPSecretByUsername, username).Scan(&secret); err != nil { if err := p.db.QueryRow(p.sqlGetTOTPSecretByUsername, username).Scan(&secret); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return "", nil return "", ErrNoTOTPSecret
} }
return "", err return "", err
} }

View File

@ -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/sign", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/register", 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/u2f/sign_request", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", 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/preferences", 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("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)

View File

@ -0,0 +1,10 @@
package utils
func IsStringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}

View File

@ -2,5 +2,5 @@
export enum SecondFactorMethod { export enum SecondFactorMethod {
TOTP = 1, TOTP = 1,
U2F = 2, U2F = 2,
Duo = 3 MobilePush = 3
} }

View File

@ -21,7 +21,8 @@ export const ResetPasswordPath = "/api/reset-password"
export const LogoutPath = "/api/logout"; export const LogoutPath = "/api/logout";
export const StatePath = "/api/state"; 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 const Available2FAMethodsPath = "/api/secondfactor/available";
export interface ErrorResponse { export interface ErrorResponse {

View File

@ -1,9 +1,9 @@
import { Get, PostWithOptionalResponse } from "./Client"; import { Get, PostWithOptionalResponse } from "./Client";
import { UserPreferencesPath } from "./Api"; import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
import { SecondFactorMethod } from "../models/Methods"; import { SecondFactorMethod } from "../models/Methods";
import { UserPreferences } from "../models/UserPreferences"; import { UserPreferences } from "../models/UserPreferences";
export type Method2FA = "u2f" | "totp" | "duo_push"; export type Method2FA = "u2f" | "totp" | "mobile_push";
export interface UserPreferencesPayload { export interface UserPreferencesPayload {
method: Method2FA; method: Method2FA;
@ -15,8 +15,8 @@ export function toEnum(method: Method2FA): SecondFactorMethod {
return SecondFactorMethod.U2F; return SecondFactorMethod.U2F;
case "totp": case "totp":
return SecondFactorMethod.TOTP; return SecondFactorMethod.TOTP;
case "duo_push": case "mobile_push":
return SecondFactorMethod.Duo; return SecondFactorMethod.MobilePush;
} }
} }
@ -26,17 +26,17 @@ export function toString(method: SecondFactorMethod): Method2FA {
return "u2f"; return "u2f";
case SecondFactorMethod.TOTP: case SecondFactorMethod.TOTP:
return "totp"; return "totp";
case SecondFactorMethod.Duo: case SecondFactorMethod.MobilePush:
return "duo_push"; return "mobile_push";
} }
} }
export async function getUserPreferences(): Promise<UserPreferences> { export async function getUserPreferences(): Promise<UserPreferences> {
const res = await Get<UserPreferencesPayload>(UserPreferencesPath); const res = await Get<UserPreferencesPayload>(UserInfoPath);
return { method: toEnum(res.method) }; return { method: toEnum(res.method) };
} }
export function setPrefered2FAMethod(method: SecondFactorMethod) { export function setPrefered2FAMethod(method: SecondFactorMethod) {
return PostWithOptionalResponse(UserPreferencesPath, return PostWithOptionalResponse(UserInfo2FAMethodPath,
{ method: toString(method) } as UserPreferencesPayload); { method: toString(method) } as UserPreferencesPayload);
} }

View File

@ -81,7 +81,7 @@ export default function () {
console.log("redirect"); console.log("redirect");
if (preferences.method === SecondFactorMethod.U2F) { if (preferences.method === SecondFactorMethod.U2F) {
redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`); redirect(`${SecondFactorU2FRoute}${redirectionSuffix}`);
} else if (preferences.method === SecondFactorMethod.Duo) { } else if (preferences.method === SecondFactorMethod.MobilePush) {
redirect(`${SecondFactorPushRoute}${redirectionSuffix}`); redirect(`${SecondFactorPushRoute}${redirectionSuffix}`);
} else { } else {
redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`); redirect(`${SecondFactorTOTPRoute}${redirectionSuffix}`);

View File

@ -42,12 +42,12 @@ export default function (props: Props) {
icon={<FingerTouchIcon size={32} />} icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.U2F)} /> onClick={() => props.onClick(SecondFactorMethod.U2F)} />
: null} : null}
{props.methods.has(SecondFactorMethod.Duo) {props.methods.has(SecondFactorMethod.MobilePush)
? <MethodItem ? <MethodItem
id="push-notification-option" id="push-notification-option"
method="Push Notification" method="Push Notification"
icon={<PushNotificationIcon width={32} height={32} />} icon={<PushNotificationIcon width={32} height={32} />}
onClick={() => props.onClick(SecondFactorMethod.Duo)} /> onClick={() => props.onClick(SecondFactorMethod.MobilePush)} />
: null} : null}
</Grid> </Grid>
</DialogContent> </DialogContent>