diff options
22 files changed, 216 insertions, 289 deletions
diff --git a/internal/handlers/handler_configuration.go b/internal/handlers/handler_configuration.go index 08d08dc79..0166cab2b 100644 --- a/internal/handlers/handler_configuration.go +++ b/internal/handlers/handler_configuration.go @@ -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. } diff --git a/internal/handlers/handler_configuration_test.go b/internal/handlers/handler_configuration_test.go index d1b64d400..d5e821013 100644 --- a/internal/handlers/handler_configuration_test.go +++ b/internal/handlers/handler_configuration_test.go @@ -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) } diff --git a/internal/handlers/handler_extended_configuration.go b/internal/handlers/handler_extended_configuration.go deleted file mode 100644 index 2a06f3e90..000000000 --- a/internal/handlers/handler_extended_configuration.go +++ /dev/null @@ -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. -} diff --git a/internal/handlers/handler_extended_configuration_test.go b/internal/handlers/handler_extended_configuration_test.go deleted file mode 100644 index b974b0758..000000000 --- a/internal/handlers/handler_extended_configuration_test.go +++ /dev/null @@ -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) -} diff --git a/internal/handlers/handler_user_info.go b/internal/handlers/handler_user_info.go index 7066c6402..410aedad5 100644 --- a/internal/handlers/handler_user_info.go +++ b/internal/handlers/handler_user_info.go @@ -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. diff --git a/internal/handlers/handler_user_info_test.go b/internal/handlers/handler_user_info_test.go index 4d925787c..44702ec28 100644 --- a/internal/handlers/handler_user_info_test.go +++ b/internal/handlers/handler_user_info_test.go @@ -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() { diff --git a/internal/handlers/types.go b/internal/handlers/types.go index b42b2889b..bcd21f62d 100644 --- a/internal/handlers/types.go +++ b/internal/handlers/types.go @@ -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"` diff --git a/internal/server/index.go b/internal/server/index.go index 9c5544f10..fbf09e9cf 100644 --- a/internal/server/index.go +++ b/internal/server/index.go @@ -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) diff --git a/internal/server/server.go b/internal/server/server.go index cee96144f..9d0bfb88d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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 != "" { diff --git a/internal/suites/scenario_backend_protection_test.go b/internal/suites/scenario_backend_protection_test.go index 197cedd86..c4b500a27 100644 --- a/internal/suites/scenario_backend_protection_test.go +++ b/internal/suites/scenario_backend_protection_test.go @@ -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) diff --git a/web/public/index.html b/web/public/index.html index e24d9350f..4ae162c2b 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -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> <!-- diff --git a/web/src/App.tsx b/web/src/App.tsx index cbe040f56..94796648b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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} /> diff --git a/web/src/hooks/BasePath.ts b/web/src/hooks/BasePath.ts index 3c2e6e425..938bea2cf 100644 --- a/web/src/hooks/BasePath.ts +++ b/web/src/hooks/BasePath.ts @@ -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"); }
\ No newline at end of file diff --git a/web/src/hooks/Configuration.ts b/web/src/hooks/Configuration.ts index 18f407596..0d445cbf3 100644 --- a/web/src/hooks/Configuration.ts +++ b/web/src/hooks/Configuration.ts @@ -1,10 +1,23 @@ import { useRemoteCall } from "./RemoteCall"; -import { getConfiguration, getExtendedConfiguration } from "../services/Configuration"; +import { getConfiguration } from "../services/Configuration"; -export function useConfiguration() { - return useRemoteCall(getConfiguration, []); +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 useExtendedConfiguration() { - return useRemoteCall(getExtendedConfiguration, []); +export function useResetPassword() { + return useEmbeddedVariable("disable-resetpassword") === "true"; +} + +export function useConfiguration() { + return useRemoteCall(getConfiguration, []); }
\ No newline at end of file diff --git a/web/src/models/Configuration.ts b/web/src/models/Configuration.ts index 56b452979..ba04e7f96 100644 --- a/web/src/models/Configuration.ts +++ b/web/src/models/Configuration.ts @@ -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; }
\ No newline at end of file diff --git a/web/src/models/UserInfo.ts b/web/src/models/UserInfo.ts index 3e10203a6..ead0752d1 100644 --- a/web/src/models/UserInfo.ts +++ b/web/src/models/UserInfo.ts @@ -1,6 +1,7 @@ import { SecondFactorMethod } from "./Methods"; export interface UserInfo { + display_name: string; method: SecondFactorMethod; has_u2f: boolean; has_totp: boolean; diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts index 77ed345b4..2f81dd343 100644 --- a/web/src/services/Api.ts +++ b/web/src/services/Api.ts @@ -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"; diff --git a/web/src/services/Configuration.ts b/web/src/services/Configuration.ts index eb0dd002e..1c9651ab7 100644 --- a/web/src/services/Configuration.ts +++ b/web/src/services/Configuration.ts @@ -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)) }; }
\ No newline at end of file diff --git a/web/src/services/UserPreferences.ts b/web/src/services/UserPreferences.ts index 471a3f304..c63db1b52 100644 --- a/web/src/services/UserPreferences.ts +++ b/web/src/services/UserPreferences.ts @@ -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; diff --git a/web/src/setupTests.js b/web/src/setupTests.js index 71a979d56..abc9fa374 100644 --- a/web/src/setupTests.js +++ b/web/src/setupTests.js @@ -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() }); diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx index e309055db..82c27ccbf 100644 --- a/web/src/views/LoginPortal/LoginPortal.tsx +++ b/web/src/views/LoginPortal/LoginPortal.tsx @@ -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} /> diff --git a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx index bc2e0ef63..6d4e25d43 100644 --- a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx +++ b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx @@ -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} |
