[MISC] Template global config and refactor some /api endpoints (#1135)
* [MISC] Template global config and refactor some /api endpoints * /api/configuration has been removed in favour of templating said global config * /api/configuration/extended has been renamed to /api/configuration and display_name has been removed * /api/user/info has been modified to include display_name Co-authored-by: Clement Michaud <clement.michaud34@gmail.com>pull/1148/head
parent
ddfce52939
commit
29e54c231b
|
@ -1,18 +1,30 @@
|
|||
package handlers
|
||||
|
||||
import "github.com/authelia/authelia/internal/middlewares"
|
||||
import (
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
// ConfigurationBody configuration parameters exposed to the frontend.
|
||||
// ConfigurationBody the content returned by the configuration endpoint.
|
||||
type ConfigurationBody struct {
|
||||
RememberMe bool `json:"remember_me"` // whether remember me is enabled or not
|
||||
ResetPassword bool `json:"reset_password"`
|
||||
AvailableMethods MethodList `json:"available_methods"`
|
||||
SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not.
|
||||
TOTPPeriod int `json:"totp_period"`
|
||||
}
|
||||
|
||||
// ConfigurationGet fetches configuration parameters for frontend mutation.
|
||||
// ConfigurationGet get the configuration accessible to authenticated users.
|
||||
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||
body := ConfigurationBody{
|
||||
RememberMe: ctx.Providers.SessionProvider.RememberMe != 0,
|
||||
ResetPassword: !ctx.Configuration.AuthenticationBackend.DisableResetPassword,
|
||||
body := ConfigurationBody{}
|
||||
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
|
||||
body.TOTPPeriod = ctx.Configuration.TOTP.Period
|
||||
|
||||
if ctx.Configuration.DuoAPI != nil {
|
||||
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
|
||||
}
|
||||
|
||||
body.SecondFactorEnabled = ctx.Providers.Authorizer.IsSecondFactorEnabled()
|
||||
ctx.Logger.Tracef("Second factor enabled: %v", body.SecondFactorEnabled)
|
||||
|
||||
ctx.Logger.Tracef("Available methods are %s", body.AvailableMethods)
|
||||
ctx.SetJSONBody(body) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
||||
}
|
||||
|
|
|
@ -5,49 +5,155 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/mocks"
|
||||
"github.com/authelia/authelia/internal/session"
|
||||
)
|
||||
|
||||
type ConfigurationSuite struct {
|
||||
type SecondFactorAvailableMethodsFixture struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *ConfigurationSuite) SetupTest() {
|
||||
func (s *SecondFactorAvailableMethodsFixture) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "deny",
|
||||
Rules: []schema.ACLRule{},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConfigurationSuite) TearDownTest() {
|
||||
func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *ConfigurationSuite) TestShouldDisableRememberMe() {
|
||||
s.mock.Ctx.Configuration.Session.RememberMeDuration = "0"
|
||||
s.mock.Ctx.Providers.SessionProvider = session.NewProvider(
|
||||
s.mock.Ctx.Configuration.Session)
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
expectedBody := ConfigurationBody{
|
||||
RememberMe: false,
|
||||
ResetPassword: true,
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
}
|
||||
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func (s *ConfigurationSuite) TestShouldDisableResetPassword() {
|
||||
s.mock.Ctx.Configuration.AuthenticationBackend.DisableResetPassword = true
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
DuoAPI: &schema.DuoAPIConfiguration{},
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
expectedBody := ConfigurationBody{
|
||||
RememberMe: true,
|
||||
ResetPassword: false,
|
||||
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
}
|
||||
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func TestRunHandlerConfigurationSuite(t *testing.T) {
|
||||
s := new(ConfigurationSuite)
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "bypass",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "single_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "two_factor",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "single_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: true,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "bypass",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "two_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: true,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunSuite(t *testing.T) {
|
||||
s := new(SecondFactorAvailableMethodsFixture)
|
||||
suite.Run(t, s)
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/authelia/authelia/internal/authentication"
|
||||
"github.com/authelia/authelia/internal/middlewares"
|
||||
)
|
||||
|
||||
// ExtendedConfigurationBody the content returned by extended configuration endpoint.
|
||||
type ExtendedConfigurationBody struct {
|
||||
AvailableMethods MethodList `json:"available_methods"`
|
||||
DisplayName string `json:"display_name"`
|
||||
SecondFactorEnabled bool `json:"second_factor_enabled"` // whether second factor is enabled or not.
|
||||
TOTPPeriod int `json:"totp_period"`
|
||||
}
|
||||
|
||||
// ExtendedConfigurationGet get the extended configuration accessible to authenticated users.
|
||||
func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
|
||||
body := ExtendedConfigurationBody{}
|
||||
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
|
||||
body.DisplayName = ctx.GetSession().DisplayName
|
||||
body.TOTPPeriod = ctx.Configuration.TOTP.Period
|
||||
|
||||
if ctx.Configuration.DuoAPI != nil {
|
||||
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
|
||||
}
|
||||
|
||||
body.SecondFactorEnabled = ctx.Providers.Authorizer.IsSecondFactorEnabled()
|
||||
ctx.Logger.Tracef("Second factor enabled: %v", body.SecondFactorEnabled)
|
||||
|
||||
ctx.Logger.Tracef("Available methods are %s", body.AvailableMethods)
|
||||
ctx.SetJSONBody(body) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
||||
}
|
|
@ -1,159 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/authelia/authelia/internal/authorization"
|
||||
"github.com/authelia/authelia/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/internal/mocks"
|
||||
)
|
||||
|
||||
type SecondFactorAvailableMethodsFixture struct {
|
||||
suite.Suite
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "deny",
|
||||
Rules: []schema.ACLRule{},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
expectedBody := ExtendedConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
}
|
||||
|
||||
ExtendedConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
DuoAPI: &schema.DuoAPIConfiguration{},
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
expectedBody := ExtendedConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
}
|
||||
|
||||
ExtendedConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), expectedBody)
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "bypass",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "single_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ExtendedConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: false,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenDefaultPolicySetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "two_factor",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "single_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ExtendedConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: true,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsEnabledWhenSomePolicySetToTwoFactor() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
TOTP: &schema.TOTPConfiguration{
|
||||
Period: schema.DefaultTOTPConfiguration.Period,
|
||||
},
|
||||
}
|
||||
s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(schema.AccessControlConfiguration{
|
||||
DefaultPolicy: "bypass",
|
||||
Rules: []schema.ACLRule{
|
||||
{
|
||||
Domains: []string{"example.com"},
|
||||
Policy: "deny",
|
||||
},
|
||||
{
|
||||
Domains: []string{"abc.example.com"},
|
||||
Policy: "two_factor",
|
||||
},
|
||||
{
|
||||
Domains: []string{"def.example.com"},
|
||||
Policy: "bypass",
|
||||
},
|
||||
},
|
||||
})
|
||||
ExtendedConfigurationGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), ExtendedConfigurationBody{
|
||||
AvailableMethods: []string{"totp", "u2f"},
|
||||
SecondFactorEnabled: true,
|
||||
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunSuite(t *testing.T) {
|
||||
s := new(SecondFactorAvailableMethodsFixture)
|
||||
suite.Run(t, s)
|
||||
}
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/authelia/authelia/internal/utils"
|
||||
)
|
||||
|
||||
func loadInfo(username string, storageProvider storage.Provider, preferences *UserPreferences, logger *logrus.Entry) []error {
|
||||
func loadInfo(username string, storageProvider storage.Provider, userInfo *UserInfo, logger *logrus.Entry) []error {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(3)
|
||||
|
@ -32,9 +32,9 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
|
|||
}
|
||||
|
||||
if method == "" {
|
||||
preferences.Method = authentication.PossibleMethods[0]
|
||||
userInfo.Method = authentication.PossibleMethods[0]
|
||||
} else {
|
||||
preferences.Method = method
|
||||
userInfo.Method = method
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -53,7 +53,7 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
|
|||
return
|
||||
}
|
||||
|
||||
preferences.HasU2F = true
|
||||
userInfo.HasU2F = true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
|
@ -71,7 +71,7 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
|
|||
return
|
||||
}
|
||||
|
||||
preferences.HasTOTP = true
|
||||
userInfo.HasTOTP = true
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
@ -83,15 +83,17 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
|
|||
func UserInfoGet(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
preferences := UserPreferences{}
|
||||
errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &preferences, ctx.Logger)
|
||||
userInfo := UserInfo{}
|
||||
errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &userInfo, ctx.Logger)
|
||||
|
||||
if len(errors) > 0 {
|
||||
ctx.Error(fmt.Errorf("Unable to load user information"), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetJSONBody(preferences) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
||||
userInfo.DisplayName = userSession.DisplayName
|
||||
|
||||
ctx.SetJSONBody(userInfo) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
|
||||
}
|
||||
|
||||
// MethodBody the selected 2FA method.
|
||||
|
|
|
@ -31,7 +31,7 @@ func (s *FetchSuite) TearDownTest() {
|
|||
s.mock.Close()
|
||||
}
|
||||
|
||||
func setPreferencesExpectations(preferences UserPreferences, provider *storage.MockProvider) {
|
||||
func setPreferencesExpectations(preferences UserInfo, provider *storage.MockProvider) {
|
||||
provider.
|
||||
EXPECT().
|
||||
LoadPreferred2FAMethod(gomock.Eq("john")).
|
||||
|
@ -65,7 +65,7 @@ func setPreferencesExpectations(preferences UserPreferences, provider *storage.M
|
|||
}
|
||||
|
||||
func TestMethodSetToU2F(t *testing.T) {
|
||||
table := []UserPreferences{
|
||||
table := []UserInfo{
|
||||
{
|
||||
Method: "totp",
|
||||
},
|
||||
|
@ -97,7 +97,7 @@ func TestMethodSetToU2F(t *testing.T) {
|
|||
setPreferencesExpectations(expectedPreferences, mock.StorageProviderMock)
|
||||
UserInfoGet(mock.Ctx)
|
||||
|
||||
actualPreferences := UserPreferences{}
|
||||
actualPreferences := UserInfo{}
|
||||
mock.GetResponseData(t, &actualPreferences)
|
||||
|
||||
t.Run("expected method", func(t *testing.T) {
|
||||
|
@ -132,7 +132,7 @@ func (s *FetchSuite) TestShouldGetDefaultPreferenceIfNotInDB() {
|
|||
Return("", storage.ErrNoTOTPSecret)
|
||||
|
||||
UserInfoGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), UserPreferences{Method: "totp"})
|
||||
s.mock.Assert200OK(s.T(), UserInfo{Method: "totp"})
|
||||
}
|
||||
|
||||
func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() {
|
||||
|
|
|
@ -11,8 +11,11 @@ type MethodList = []string
|
|||
|
||||
type authorizationMatching int
|
||||
|
||||
// UserPreferences is the model of user second factor preferences.
|
||||
type UserPreferences struct {
|
||||
// UserInfo is the model of user info and second factor preferences.
|
||||
type UserInfo struct {
|
||||
// The users display name.
|
||||
DisplayName string `json:"display_name"`
|
||||
|
||||
// The preferred 2FA method.
|
||||
Method string `json:"method" valid:"required"`
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV
|
|||
// ServeIndex serve the index.html file with nonce generated for supporting
|
||||
// restrictive CSP while using material-ui from the embedded virtual filesystem.
|
||||
//go:generate broccoli -src ../../public_html -o public_html
|
||||
func ServeIndex(publicDir, base string) fasthttp.RequestHandler {
|
||||
func ServeIndex(publicDir, base, rememberMe, resetPassword string) fasthttp.RequestHandler {
|
||||
f, err := br.Open(publicDir + "/index.html")
|
||||
if err != nil {
|
||||
logging.Logger().Fatalf("Unable to open index.html: %v", err)
|
||||
|
@ -38,7 +38,7 @@ func ServeIndex(publicDir, base string) fasthttp.RequestHandler {
|
|||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self'; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
|
||||
|
||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ CSPNonce, Base string }{CSPNonce: nonce, Base: base})
|
||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, RememberMe, ResetPassword string }{Base: base, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword})
|
||||
if err != nil {
|
||||
ctx.Error("An error occurred", 503)
|
||||
logging.Logger().Errorf("Unable to execute template: %v", err)
|
||||
|
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
duoapi "github.com/duosecurity/duo_api_golang"
|
||||
"github.com/fasthttp/router"
|
||||
|
@ -22,10 +23,15 @@ import (
|
|||
func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
|
||||
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
|
||||
embeddedAssets := "/public_html"
|
||||
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0")
|
||||
resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword)
|
||||
|
||||
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
|
||||
|
||||
serveIndexHandler := ServeIndex(embeddedAssets, configuration.Server.Path, rememberMe, resetPassword)
|
||||
|
||||
r := router.New()
|
||||
r.GET("/", ServeIndex(embeddedAssets, configuration.Server.Path))
|
||||
r.GET("/", serveIndexHandler)
|
||||
|
||||
for _, f := range rootFiles {
|
||||
r.GET("/"+f, fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets)))
|
||||
|
@ -35,9 +41,8 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
|||
|
||||
r.GET("/api/state", autheliaMiddleware(handlers.StateGet))
|
||||
|
||||
r.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet))
|
||||
r.GET("/api/configuration/extended", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
|
||||
r.GET("/api/configuration", autheliaMiddleware(
|
||||
middlewares.RequireFirstFactor(handlers.ConfigurationGet)))
|
||||
|
||||
r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||
r.HEAD("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
|
||||
|
@ -113,7 +118,7 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
|||
r.GET("/debug/vars", expvarhandler.ExpvarHandler)
|
||||
}
|
||||
|
||||
r.NotFound = ServeIndex(embeddedAssets, configuration.Server.Path)
|
||||
r.NotFound = serveIndexHandler
|
||||
|
||||
handler := middlewares.LogRequestMiddleware(r.Handler)
|
||||
if configuration.Server.Path != "" {
|
||||
|
|
|
@ -47,10 +47,7 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
|
|||
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403)
|
||||
|
||||
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/user/info", AutheliaBaseURL), 403)
|
||||
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration/extended", AutheliaBaseURL), 403)
|
||||
|
||||
// This is the global configuration, it's safe to let it open.
|
||||
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", AutheliaBaseURL), 200)
|
||||
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", 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/finish", AutheliaBaseURL), 403)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<title>Login - Authelia</title>
|
||||
</head>
|
||||
|
||||
<body data-basepath="%PUBLIC_URL%">
|
||||
<body data-basepath="%PUBLIC_URL%" data-rememberme="{{.RememberMe}}" data-disable-resetpassword="{{.ResetPassword}}">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router, Route, Switch, Redirect
|
||||
} from "react-router-dom";
|
||||
|
@ -17,7 +17,7 @@ import NotificationsContext from './hooks/NotificationsContext';
|
|||
import { Notification } from './models/Notifications';
|
||||
import NotificationBar from './components/NotificationBar';
|
||||
import SignOut from './views/LoginPortal/SignOut/SignOut';
|
||||
import { useConfiguration } from './hooks/Configuration';
|
||||
import { useRememberMe, useResetPassword } from './hooks/Configuration';
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||
import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
|
||||
import { useBasePath } from './hooks/BasePath';
|
||||
|
@ -26,15 +26,6 @@ faConfig.autoAddCss = false;
|
|||
|
||||
const App: React.FC = () => {
|
||||
const [notification, setNotification] = useState(null as Notification | null);
|
||||
const [configuration, fetchConfig, , fetchConfigError] = useConfiguration();
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchConfigError) {
|
||||
console.error(fetchConfigError);
|
||||
}
|
||||
}, [fetchConfigError]);
|
||||
|
||||
useEffect(() => { fetchConfig() }, [fetchConfig]);
|
||||
|
||||
return (
|
||||
<NotificationsContext.Provider value={{ notification, setNotification }} >
|
||||
|
@ -58,8 +49,8 @@ const App: React.FC = () => {
|
|||
</Route>
|
||||
<Route path={FirstFactorRoute}>
|
||||
<LoginPortal
|
||||
rememberMe={configuration?.remember_me === true}
|
||||
resetPassword={configuration?.reset_password === true} />
|
||||
rememberMe={useRememberMe()}
|
||||
resetPassword={useResetPassword()} />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to={FirstFactorRoute} />
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
export function useBasePath() {
|
||||
const basePath = document.body.getAttribute("data-basepath");
|
||||
if (basePath === null) {
|
||||
throw new Error("No base path detected");
|
||||
}
|
||||
import { useEmbeddedVariable } from "./Configuration";
|
||||
|
||||
return basePath;
|
||||
export function useBasePath() {
|
||||
return useEmbeddedVariable("basepath");
|
||||
}
|
|
@ -1,10 +1,23 @@
|
|||
import { useRemoteCall } from "./RemoteCall";
|
||||
import { getConfiguration, getExtendedConfiguration } from "../services/Configuration";
|
||||
import { getConfiguration } from "../services/Configuration";
|
||||
|
||||
export function useEmbeddedVariable(variableName: string) {
|
||||
const value = document.body.getAttribute(`data-${variableName}`);
|
||||
if (value === null) {
|
||||
throw new Error(`No ${variableName} embedded variable detected`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useRememberMe() {
|
||||
return useEmbeddedVariable("rememberme") === "true";
|
||||
}
|
||||
|
||||
export function useResetPassword() {
|
||||
return useEmbeddedVariable("disable-resetpassword") === "true";
|
||||
}
|
||||
|
||||
export function useConfiguration() {
|
||||
return useRemoteCall(getConfiguration, []);
|
||||
}
|
||||
|
||||
export function useExtendedConfiguration() {
|
||||
return useRemoteCall(getExtendedConfiguration, []);
|
||||
}
|
|
@ -1,13 +1,7 @@
|
|||
import { SecondFactorMethod } from "./Methods";
|
||||
|
||||
export interface Configuration {
|
||||
remember_me: boolean;
|
||||
reset_password: boolean;
|
||||
}
|
||||
|
||||
export interface ExtendedConfiguration {
|
||||
available_methods: Set<SecondFactorMethod>;
|
||||
display_name: string;
|
||||
second_factor_enabled: boolean;
|
||||
totp_period: number;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { SecondFactorMethod } from "./Methods";
|
||||
|
||||
export interface UserInfo {
|
||||
display_name: string;
|
||||
method: SecondFactorMethod;
|
||||
has_u2f: boolean;
|
||||
has_totp: boolean;
|
||||
|
|
|
@ -28,7 +28,6 @@ export const UserInfoPath = basePath + "/api/user/info";
|
|||
export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
|
||||
|
||||
export const ConfigurationPath = basePath + "/api/configuration";
|
||||
export const ExtendedConfigurationPath = basePath + "/api/configuration/extended";
|
||||
|
||||
export interface ErrorResponse {
|
||||
status: "KO";
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
import { Get } from "./Client";
|
||||
import { ExtendedConfigurationPath, ConfigurationPath } from "./Api";
|
||||
import { ConfigurationPath } from "./Api";
|
||||
import { toEnum, Method2FA } from "./UserPreferences";
|
||||
import { Configuration, ExtendedConfiguration } from "../models/Configuration";
|
||||
import { Configuration } from "../models/Configuration";
|
||||
|
||||
export async function getConfiguration(): Promise<Configuration> {
|
||||
return Get<Configuration>(ConfigurationPath);
|
||||
}
|
||||
|
||||
interface ExtendedConfigurationPayload {
|
||||
interface ConfigurationPayload {
|
||||
available_methods: Method2FA[];
|
||||
display_name: string;
|
||||
second_factor_enabled: boolean;
|
||||
totp_period: number;
|
||||
}
|
||||
|
||||
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
||||
const config = await Get<ExtendedConfigurationPayload>(ExtendedConfigurationPath);
|
||||
export async function getConfiguration(): Promise<Configuration> {
|
||||
const config = await Get<ConfigurationPayload>(ConfigurationPath);
|
||||
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
|
||||
}
|
|
@ -6,6 +6,7 @@ import { UserInfo } from "../models/UserInfo";
|
|||
export type Method2FA = "u2f" | "totp" | "mobile_push";
|
||||
|
||||
export interface UserInfoPayload {
|
||||
display_name: string;
|
||||
method: Method2FA;
|
||||
has_u2f: boolean;
|
||||
has_totp: boolean;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
document.body.setAttribute("data-basepath", "");
|
||||
document.body.setAttribute("data-rememberme", "false");
|
||||
document.body.setAttribute("data-disable-resetpassword", "false");
|
||||
configure({ adapter: new Adapter() });
|
||||
|
|
|
@ -13,7 +13,7 @@ import { useNotifications } from "../../hooks/NotificationsContext";
|
|||
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
||||
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
||||
import { SecondFactorMethod } from "../../models/Methods";
|
||||
import { useExtendedConfiguration } from "../../hooks/Configuration";
|
||||
import { useConfiguration } from "../../hooks/Configuration";
|
||||
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
||||
|
||||
export interface Props {
|
||||
|
@ -30,7 +30,7 @@ export default function (props: Props) {
|
|||
|
||||
const [state, fetchState, , fetchStateError] = useAutheliaState();
|
||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
|
||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration();
|
||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
|
||||
|
||||
const redirect = useCallback((url: string) => history.push(url), [history]);
|
||||
|
||||
|
@ -135,7 +135,7 @@ export default function (props: Props) {
|
|||
onAuthenticationSuccess={handleAuthSuccess} /> : null}
|
||||
</Route>
|
||||
<Route path={AuthenticatedRoute} exact>
|
||||
{configuration ? <AuthenticatedView name={configuration.display_name} /> : null}
|
||||
{userInfo ? <AuthenticatedView name={userInfo.display_name} /> : null}
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to={FirstFactorRoute} />
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
} from "../../../Routes";
|
||||
import { setPreferred2FAMethod } from "../../../services/UserPreferences";
|
||||
import { UserInfo } from "../../../models/UserInfo";
|
||||
import { ExtendedConfiguration } from "../../../models/Configuration";
|
||||
import { Configuration } from "../../../models/Configuration";
|
||||
import u2fApi from "u2f-api";
|
||||
import { AuthenticationLevel } from "../../../services/State";
|
||||
|
||||
|
@ -28,7 +28,7 @@ export interface Props {
|
|||
authenticationLevel: AuthenticationLevel;
|
||||
|
||||
userInfo: UserInfo;
|
||||
configuration: ExtendedConfiguration;
|
||||
configuration: Configuration;
|
||||
|
||||
onMethodChanged: (method: SecondFactorMethod) => void;
|
||||
onAuthenticationSuccess: (redirectURL: string | undefined) => void;
|
||||
|
@ -88,7 +88,7 @@ export default function (props: Props) {
|
|||
return (
|
||||
<LoginLayout
|
||||
id="second-factor-stage"
|
||||
title={`Hi ${props.configuration.display_name}`}
|
||||
title={`Hi ${props.userInfo.display_name}`}
|
||||
showBrand>
|
||||
<MethodSelectionDialog
|
||||
open={methodSelectionOpen}
|
||||
|
|
Loading…
Reference in New Issue