[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>
This commit is contained in:
Amir Zarrinkafsh 2020-06-21 23:40:37 +10:00 committed by GitHub
parent ddfce52939
commit 29e54c231b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 216 additions and 289 deletions

View File

@ -1,18 +1,30 @@
package handlers 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 { type ConfigurationBody struct {
RememberMe bool `json:"remember_me"` // whether remember me is enabled or not AvailableMethods MethodList `json:"available_methods"`
ResetPassword bool `json:"reset_password"` 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) { func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{ body := ConfigurationBody{}
RememberMe: ctx.Providers.SessionProvider.RememberMe != 0, body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
ResetPassword: !ctx.Configuration.AuthenticationBackend.DisableResetPassword, 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. ctx.SetJSONBody(body) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
} }

View File

@ -5,49 +5,155 @@ import (
"github.com/stretchr/testify/suite" "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/mocks"
"github.com/authelia/authelia/internal/session"
) )
type ConfigurationSuite struct { type SecondFactorAvailableMethodsFixture struct {
suite.Suite suite.Suite
mock *mocks.MockAutheliaCtx mock *mocks.MockAutheliaCtx
} }
func (s *ConfigurationSuite) SetupTest() { func (s *SecondFactorAvailableMethodsFixture) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T()) 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() s.mock.Close()
} }
func (s *ConfigurationSuite) TestShouldDisableRememberMe() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
s.mock.Ctx.Configuration.Session.RememberMeDuration = "0" s.mock.Ctx.Configuration = schema.Configuration{
s.mock.Ctx.Providers.SessionProvider = session.NewProvider( TOTP: &schema.TOTPConfiguration{
s.mock.Ctx.Configuration.Session) Period: schema.DefaultTOTPConfiguration.Period,
},
}
expectedBody := ConfigurationBody{ expectedBody := ConfigurationBody{
RememberMe: false, AvailableMethods: []string{"totp", "u2f"},
ResetPassword: true, SecondFactorEnabled: false,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody) s.mock.Assert200OK(s.T(), expectedBody)
} }
func (s *ConfigurationSuite) TestShouldDisableResetPassword() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration.AuthenticationBackend.DisableResetPassword = true s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
TOTP: &schema.TOTPConfiguration{
Period: schema.DefaultTOTPConfiguration.Period,
},
}
expectedBody := ConfigurationBody{ expectedBody := ConfigurationBody{
RememberMe: true, AvailableMethods: []string{"totp", "u2f", "mobile_push"},
ResetPassword: false, SecondFactorEnabled: false,
TOTPPeriod: schema.DefaultTOTPConfiguration.Period,
} }
ConfigurationGet(s.mock.Ctx) ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody) s.mock.Assert200OK(s.T(), expectedBody)
} }
func TestRunHandlerConfigurationSuite(t *testing.T) { func (s *SecondFactorAvailableMethodsFixture) TestShouldCheckSecondFactorIsDisabledWhenNoRuleIsSetToTwoFactor() {
s := new(ConfigurationSuite) 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) suite.Run(t, s)
} }

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import (
"github.com/authelia/authelia/internal/utils" "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 var wg sync.WaitGroup
wg.Add(3) wg.Add(3)
@ -32,9 +32,9 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
} }
if method == "" { if method == "" {
preferences.Method = authentication.PossibleMethods[0] userInfo.Method = authentication.PossibleMethods[0]
} else { } else {
preferences.Method = method userInfo.Method = method
} }
}() }()
@ -53,7 +53,7 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
return return
} }
preferences.HasU2F = true userInfo.HasU2F = true
}() }()
go func() { go func() {
@ -71,7 +71,7 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
return return
} }
preferences.HasTOTP = true userInfo.HasTOTP = true
}() }()
wg.Wait() wg.Wait()
@ -83,15 +83,17 @@ func loadInfo(username string, storageProvider storage.Provider, preferences *Us
func UserInfoGet(ctx *middlewares.AutheliaCtx) { func UserInfoGet(ctx *middlewares.AutheliaCtx) {
userSession := ctx.GetSession() userSession := ctx.GetSession()
preferences := UserPreferences{} userInfo := UserInfo{}
errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &preferences, ctx.Logger) errors := loadInfo(userSession.Username, ctx.Providers.StorageProvider, &userInfo, ctx.Logger)
if len(errors) > 0 { if len(errors) > 0 {
ctx.Error(fmt.Errorf("Unable to load user information"), operationFailedMessage) ctx.Error(fmt.Errorf("Unable to load user information"), operationFailedMessage)
return 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. // MethodBody the selected 2FA method.

View File

@ -31,7 +31,7 @@ func (s *FetchSuite) TearDownTest() {
s.mock.Close() s.mock.Close()
} }
func setPreferencesExpectations(preferences UserPreferences, provider *storage.MockProvider) { func setPreferencesExpectations(preferences UserInfo, provider *storage.MockProvider) {
provider. provider.
EXPECT(). EXPECT().
LoadPreferred2FAMethod(gomock.Eq("john")). LoadPreferred2FAMethod(gomock.Eq("john")).
@ -65,7 +65,7 @@ func setPreferencesExpectations(preferences UserPreferences, provider *storage.M
} }
func TestMethodSetToU2F(t *testing.T) { func TestMethodSetToU2F(t *testing.T) {
table := []UserPreferences{ table := []UserInfo{
{ {
Method: "totp", Method: "totp",
}, },
@ -97,7 +97,7 @@ func TestMethodSetToU2F(t *testing.T) {
setPreferencesExpectations(expectedPreferences, mock.StorageProviderMock) setPreferencesExpectations(expectedPreferences, mock.StorageProviderMock)
UserInfoGet(mock.Ctx) UserInfoGet(mock.Ctx)
actualPreferences := UserPreferences{} actualPreferences := UserInfo{}
mock.GetResponseData(t, &actualPreferences) mock.GetResponseData(t, &actualPreferences)
t.Run("expected method", func(t *testing.T) { t.Run("expected method", func(t *testing.T) {
@ -132,7 +132,7 @@ func (s *FetchSuite) TestShouldGetDefaultPreferenceIfNotInDB() {
Return("", storage.ErrNoTOTPSecret) Return("", storage.ErrNoTOTPSecret)
UserInfoGet(s.mock.Ctx) UserInfoGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), UserPreferences{Method: "totp"}) s.mock.Assert200OK(s.T(), UserInfo{Method: "totp"})
} }
func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() { func (s *FetchSuite) TestShouldReturnError500WhenStorageFailsToLoad() {

View File

@ -11,8 +11,11 @@ type MethodList = []string
type authorizationMatching int type authorizationMatching int
// UserPreferences is the model of user second factor preferences. // UserInfo is the model of user info and second factor preferences.
type UserPreferences struct { type UserInfo struct {
// The users display name.
DisplayName string `json:"display_name"`
// The preferred 2FA method. // The preferred 2FA method.
Method string `json:"method" valid:"required"` Method string `json:"method" valid:"required"`

View File

@ -16,7 +16,7 @@ var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV
// ServeIndex serve the index.html file with nonce generated for supporting // ServeIndex serve the index.html file with nonce generated for supporting
// restrictive CSP while using material-ui from the embedded virtual filesystem. // restrictive CSP while using material-ui from the embedded virtual filesystem.
//go:generate broccoli -src ../../public_html -o public_html //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") f, err := br.Open(publicDir + "/index.html")
if err != nil { if err != nil {
logging.Logger().Fatalf("Unable to open index.html: %v", err) 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.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)) 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 { if err != nil {
ctx.Error("An error occurred", 503) ctx.Error("An error occurred", 503)
logging.Logger().Errorf("Unable to execute template: %v", err) logging.Logger().Errorf("Unable to execute template: %v", err)

View File

@ -3,6 +3,7 @@ package server
import ( import (
"fmt" "fmt"
"os" "os"
"strconv"
duoapi "github.com/duosecurity/duo_api_golang" duoapi "github.com/duosecurity/duo_api_golang"
"github.com/fasthttp/router" "github.com/fasthttp/router"
@ -22,10 +23,15 @@ import (
func StartServer(configuration schema.Configuration, providers middlewares.Providers) { func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers) autheliaMiddleware := middlewares.AutheliaMiddleware(configuration, providers)
embeddedAssets := "/public_html" embeddedAssets := "/public_html"
rememberMe := strconv.FormatBool(configuration.Session.RememberMeDuration != "0")
resetPassword := strconv.FormatBool(!configuration.AuthenticationBackend.DisableResetPassword)
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"} rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
serveIndexHandler := ServeIndex(embeddedAssets, configuration.Server.Path, rememberMe, resetPassword)
r := router.New() r := router.New()
r.GET("/", ServeIndex(embeddedAssets, configuration.Server.Path)) r.GET("/", serveIndexHandler)
for _, f := range rootFiles { for _, f := range rootFiles {
r.GET("/"+f, fasthttpadaptor.NewFastHTTPHandler(br.Serve(embeddedAssets))) 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/state", autheliaMiddleware(handlers.StateGet))
r.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet)) r.GET("/api/configuration", autheliaMiddleware(
r.GET("/api/configuration/extended", autheliaMiddleware( middlewares.RequireFirstFactor(handlers.ConfigurationGet)))
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend))) r.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet(configuration.AuthenticationBackend)))
r.HEAD("/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.GET("/debug/vars", expvarhandler.ExpvarHandler)
} }
r.NotFound = ServeIndex(embeddedAssets, configuration.Server.Path) r.NotFound = serveIndexHandler
handler := middlewares.LogRequestMiddleware(r.Handler) handler := middlewares.LogRequestMiddleware(r.Handler)
if configuration.Server.Path != "" { if configuration.Server.Path != "" {

View File

@ -47,10 +47,7 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403) 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/user/info", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration/extended", AutheliaBaseURL), 403) s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", 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("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", 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) s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)

View File

@ -25,7 +25,7 @@
<title>Login - Authelia</title> <title>Login - Authelia</title>
</head> </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> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { import {
BrowserRouter as Router, Route, Switch, Redirect BrowserRouter as Router, Route, Switch, Redirect
} from "react-router-dom"; } from "react-router-dom";
@ -17,7 +17,7 @@ import NotificationsContext from './hooks/NotificationsContext';
import { Notification } from './models/Notifications'; import { Notification } from './models/Notifications';
import NotificationBar from './components/NotificationBar'; import NotificationBar from './components/NotificationBar';
import SignOut from './views/LoginPortal/SignOut/SignOut'; 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 '@fortawesome/fontawesome-svg-core/styles.css'
import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
import { useBasePath } from './hooks/BasePath'; import { useBasePath } from './hooks/BasePath';
@ -26,15 +26,6 @@ faConfig.autoAddCss = false;
const App: React.FC = () => { const App: React.FC = () => {
const [notification, setNotification] = useState(null as Notification | null); const [notification, setNotification] = useState(null as Notification | null);
const [configuration, fetchConfig, , fetchConfigError] = useConfiguration();
useEffect(() => {
if (fetchConfigError) {
console.error(fetchConfigError);
}
}, [fetchConfigError]);
useEffect(() => { fetchConfig() }, [fetchConfig]);
return ( return (
<NotificationsContext.Provider value={{ notification, setNotification }} > <NotificationsContext.Provider value={{ notification, setNotification }} >
@ -58,8 +49,8 @@ const App: React.FC = () => {
</Route> </Route>
<Route path={FirstFactorRoute}> <Route path={FirstFactorRoute}>
<LoginPortal <LoginPortal
rememberMe={configuration?.remember_me === true} rememberMe={useRememberMe()}
resetPassword={configuration?.reset_password === true} /> resetPassword={useResetPassword()} />
</Route> </Route>
<Route path="/"> <Route path="/">
<Redirect to={FirstFactorRoute} /> <Redirect to={FirstFactorRoute} />

View File

@ -1,8 +1,5 @@
export function useBasePath() { import { useEmbeddedVariable } from "./Configuration";
const basePath = document.body.getAttribute("data-basepath");
if (basePath === null) {
throw new Error("No base path detected");
}
return basePath; export function useBasePath() {
return useEmbeddedVariable("basepath");
} }

View File

@ -1,10 +1,23 @@
import { useRemoteCall } from "./RemoteCall"; 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() { export function useConfiguration() {
return useRemoteCall(getConfiguration, []); return useRemoteCall(getConfiguration, []);
}
export function useExtendedConfiguration() {
return useRemoteCall(getExtendedConfiguration, []);
} }

View File

@ -1,13 +1,7 @@
import { SecondFactorMethod } from "./Methods"; import { SecondFactorMethod } from "./Methods";
export interface Configuration { export interface Configuration {
remember_me: boolean;
reset_password: boolean;
}
export interface ExtendedConfiguration {
available_methods: Set<SecondFactorMethod>; available_methods: Set<SecondFactorMethod>;
display_name: string;
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number; totp_period: number;
} }

View File

@ -1,6 +1,7 @@
import { SecondFactorMethod } from "./Methods"; import { SecondFactorMethod } from "./Methods";
export interface UserInfo { export interface UserInfo {
display_name: string;
method: SecondFactorMethod; method: SecondFactorMethod;
has_u2f: boolean; has_u2f: boolean;
has_totp: boolean; has_totp: boolean;

View File

@ -28,7 +28,6 @@ export const UserInfoPath = basePath + "/api/user/info";
export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method"; export const UserInfo2FAMethodPath = basePath + "/api/user/info/2fa_method";
export const ConfigurationPath = basePath + "/api/configuration"; export const ConfigurationPath = basePath + "/api/configuration";
export const ExtendedConfigurationPath = basePath + "/api/configuration/extended";
export interface ErrorResponse { export interface ErrorResponse {
status: "KO"; status: "KO";

View File

@ -1,20 +1,15 @@
import { Get } from "./Client"; import { Get } from "./Client";
import { ExtendedConfigurationPath, ConfigurationPath } from "./Api"; import { ConfigurationPath } from "./Api";
import { toEnum, Method2FA } from "./UserPreferences"; import { toEnum, Method2FA } from "./UserPreferences";
import { Configuration, ExtendedConfiguration } from "../models/Configuration"; import { Configuration } from "../models/Configuration";
export async function getConfiguration(): Promise<Configuration> { interface ConfigurationPayload {
return Get<Configuration>(ConfigurationPath);
}
interface ExtendedConfigurationPayload {
available_methods: Method2FA[]; available_methods: Method2FA[];
display_name: string;
second_factor_enabled: boolean; second_factor_enabled: boolean;
totp_period: number; totp_period: number;
} }
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> { export async function getConfiguration(): Promise<Configuration> {
const config = await Get<ExtendedConfigurationPayload>(ExtendedConfigurationPath); const config = await Get<ConfigurationPayload>(ConfigurationPath);
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) }; return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
} }

View File

@ -6,6 +6,7 @@ import { UserInfo } from "../models/UserInfo";
export type Method2FA = "u2f" | "totp" | "mobile_push"; export type Method2FA = "u2f" | "totp" | "mobile_push";
export interface UserInfoPayload { export interface UserInfoPayload {
display_name: string;
method: Method2FA; method: Method2FA;
has_u2f: boolean; has_u2f: boolean;
has_totp: boolean; has_totp: boolean;

View File

@ -1,4 +1,6 @@
import { configure } from 'enzyme'; import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
document.body.setAttribute("data-basepath", ""); document.body.setAttribute("data-basepath", "");
document.body.setAttribute("data-rememberme", "false");
document.body.setAttribute("data-disable-resetpassword", "false");
configure({ adapter: new Adapter() }); configure({ adapter: new Adapter() });

View File

@ -13,7 +13,7 @@ import { useNotifications } from "../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../hooks/RedirectionURL"; import { useRedirectionURL } from "../../hooks/RedirectionURL";
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo"; import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
import { SecondFactorMethod } from "../../models/Methods"; import { SecondFactorMethod } from "../../models/Methods";
import { useExtendedConfiguration } from "../../hooks/Configuration"; import { useConfiguration } from "../../hooks/Configuration";
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView"; import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
export interface Props { export interface Props {
@ -30,7 +30,7 @@ export default function (props: Props) {
const [state, fetchState, , fetchStateError] = useAutheliaState(); const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo(); const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration(); const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
const redirect = useCallback((url: string) => history.push(url), [history]); const redirect = useCallback((url: string) => history.push(url), [history]);
@ -135,7 +135,7 @@ export default function (props: Props) {
onAuthenticationSuccess={handleAuthSuccess} /> : null} onAuthenticationSuccess={handleAuthSuccess} /> : null}
</Route> </Route>
<Route path={AuthenticatedRoute} exact> <Route path={AuthenticatedRoute} exact>
{configuration ? <AuthenticatedView name={configuration.display_name} /> : null} {userInfo ? <AuthenticatedView name={userInfo.display_name} /> : null}
</Route> </Route>
<Route path="/"> <Route path="/">
<Redirect to={FirstFactorRoute} /> <Redirect to={FirstFactorRoute} />

View File

@ -18,7 +18,7 @@ import {
} from "../../../Routes"; } from "../../../Routes";
import { setPreferred2FAMethod } from "../../../services/UserPreferences"; import { setPreferred2FAMethod } from "../../../services/UserPreferences";
import { UserInfo } from "../../../models/UserInfo"; import { UserInfo } from "../../../models/UserInfo";
import { ExtendedConfiguration } from "../../../models/Configuration"; import { Configuration } from "../../../models/Configuration";
import u2fApi from "u2f-api"; import u2fApi from "u2f-api";
import { AuthenticationLevel } from "../../../services/State"; import { AuthenticationLevel } from "../../../services/State";
@ -28,7 +28,7 @@ export interface Props {
authenticationLevel: AuthenticationLevel; authenticationLevel: AuthenticationLevel;
userInfo: UserInfo; userInfo: UserInfo;
configuration: ExtendedConfiguration; configuration: Configuration;
onMethodChanged: (method: SecondFactorMethod) => void; onMethodChanged: (method: SecondFactorMethod) => void;
onAuthenticationSuccess: (redirectURL: string | undefined) => void; onAuthenticationSuccess: (redirectURL: string | undefined) => void;
@ -88,7 +88,7 @@ export default function (props: Props) {
return ( return (
<LoginLayout <LoginLayout
id="second-factor-stage" id="second-factor-stage"
title={`Hi ${props.configuration.display_name}`} title={`Hi ${props.userInfo.display_name}`}
showBrand> showBrand>
<MethodSelectionDialog <MethodSelectionDialog
open={methodSelectionOpen} open={methodSelectionOpen}