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

View File

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

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"
)
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.

View File

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

View File

@ -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"`

View File

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

View File

@ -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 != "" {

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("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)

View File

@ -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>
<!--

View File

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

View File

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

View File

@ -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, []);
}

View File

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

View File

@ -1,6 +1,7 @@
import { SecondFactorMethod } from "./Methods";
export interface UserInfo {
display_name: string;
method: SecondFactorMethod;
has_u2f: 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 ConfigurationPath = basePath + "/api/configuration";
export const ExtendedConfigurationPath = basePath + "/api/configuration/extended";
export interface ErrorResponse {
status: "KO";

View File

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

View File

@ -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;

View File

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

View File

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

View File

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