Allow administrator to provide a Google Analytics tracking ID.

Providing a GA tracking ID allows administrators to analyze
how the portal is used by their users in large environments,
i.e., with many users.
This will make even more sense when we have users and admins
management interfaces.
pull/482/head
Clement Michaud 2019-12-07 17:40:42 +01:00 committed by Clément Michaud
parent 3faa63e8ed
commit 3d20142292
38 changed files with 306 additions and 159 deletions

View File

@ -26,6 +26,11 @@ jwt_secret: a_very_important_secret
# be redirected upon successful authentication.
default_redirection_url: https://home.example.com:8080/
# Google Analytics Tracking ID to track the usage of the portal
# using a Google Analytics dashboard.
#
## google_analytics: UA-00000-01
# TOTP Issuer Name
#
# This will be the issuer name displayed in Google Authenticator

View File

@ -7,4 +7,11 @@ ARG GROUP_ID
RUN addgroup --gid ${GROUP_ID} dev && \
adduser --uid ${USER_ID} -G dev -D dev
RUN mkdir -p /etc/authelia && chown dev:dev /etc/authelia
RUN mkdir -p /var/lib/authelia && chown dev:dev /var/lib/authelia
USER dev
VOLUME /etc/authelia
VOLUME /var/lib/authelia

View File

@ -4,8 +4,6 @@ services:
build:
context: .
dockerfile: Dockerfile
volumes:
- "/tmp/authelia:/tmp/authelia"
environment:
- ENVIRONMENT=dev
restart: always

View File

@ -13,7 +13,6 @@ services:
- "./example/compose/authelia/resources/:/resources"
- ".:/app"
- "${GOPATH}:/go"
- "/tmp/authelia:/tmp/authelia"
environment:
- ENVIRONMENT=dev
networks:

View File

@ -4,9 +4,6 @@ set -x
go get github.com/cespare/reflex
mkdir -p /var/lib/authelia
mkdir -p /etc/authelia
# Sleep 10 seconds to wait the end of npm install updating web directory
# and making reflex reload multiple times.
sleep 10

View File

@ -5,8 +5,6 @@ services:
volumes:
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
- ./example/compose/nginx/portal/ssl:/etc/ssl
ports:
- "8080:8080"
networks:
authelianet:
aliases:

View File

@ -2,10 +2,12 @@ package schema
// Configuration object extracted from YAML configuration file.
type Configuration struct {
Port int `yaml:"port"`
LogsLevel string `yaml:"logs_level"`
JWTSecret string `yaml:"jwt_secret"`
DefaultRedirectionURL string `yaml:"default_redirection_url"`
Port int `yaml:"port"`
LogsLevel string `yaml:"logs_level"`
JWTSecret string `yaml:"jwt_secret"`
DefaultRedirectionURL string `yaml:"default_redirection_url"`
GoogleAnalyticsTrackingID string `yaml:"google_analytics"`
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
Session SessionConfiguration `yaml:"session"`

View File

@ -1,19 +0,0 @@
package handlers
import (
"github.com/clems4ever/authelia/internal/authentication"
"github.com/clems4ever/authelia/internal/middlewares"
)
// SecondFactorAvailableMethodsGet retrieve available 2FA methods.
// The supported methods are: "totp", "u2f", "duo"
func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) {
availableMethods := MethodList{authentication.TOTP, authentication.U2F}
if ctx.Configuration.DuoAPI != nil {
availableMethods = append(availableMethods, authentication.Push)
}
ctx.Logger.Debugf("Available methods are %s", availableMethods)
ctx.SetJSONBody(availableMethods)
}

View File

@ -0,0 +1,14 @@
package handlers
import "github.com/clems4ever/authelia/internal/middlewares"
type ConfigurationBody struct {
GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
}
func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ConfigurationBody{
GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
}
ctx.SetJSONBody(body)
}

View File

@ -0,0 +1,32 @@
package handlers
import (
"github.com/clems4ever/authelia/internal/mocks"
"github.com/stretchr/testify/suite"
)
type ConfigurationSuite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *ConfigurationSuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
}
func (s *ConfigurationSuite) TearDownTest() {
s.mock.Close()
}
func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
GATrackingID := "ABC"
s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
expectedBody := ConfigurationBody{
GoogleAnalyticsTrackingID: GATrackingID,
}
ConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}

View File

@ -0,0 +1,23 @@
package handlers
import (
"github.com/clems4ever/authelia/internal/authentication"
"github.com/clems4ever/authelia/internal/middlewares"
)
type ExtendedConfigurationBody struct {
AvailableMethods MethodList `json:"available_methods"`
}
// ExtendedConfigurationGet get the extended configuration accessbile to authenticated users.
func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
body := ExtendedConfigurationBody{}
body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
if ctx.Configuration.DuoAPI != nil {
body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
}
ctx.Logger.Debugf("Available methods are %s", body.AvailableMethods)
ctx.SetJSONBody(body)
}

View File

@ -11,7 +11,6 @@ import (
type SecondFactorAvailableMethodsFixture struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
@ -24,16 +23,22 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
SecondFactorAvailableMethodsGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), []string{"totp", "u2f"})
expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f"},
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
}
SecondFactorAvailableMethodsGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), []string{"totp", "u2f", "mobile_push"})
expectedBody := ExtendedConfigurationBody{
AvailableMethods: []string{"totp", "u2f", "mobile_push"},
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
}
func TestRunSuite(t *testing.T) {

View File

@ -32,6 +32,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.GET("/api/state", autheliaMiddleware(handlers.StateGet))
router.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet))
router.GET("/api/configuration/extended", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
@ -45,10 +49,6 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.POST("/api/reset-password", autheliaMiddleware(
handlers.ResetPasswordPost))
// 2FA preferences and settings related endpoints.
router.GET("/api/secondfactor/available", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet)))
// Information about the user
router.GET("/api/user/info", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserInfoGet)))

View File

@ -20,7 +20,7 @@ session:
storage:
local:
path: /tmp/authelia/db.sqlite
path: /var/lib/authelia/db.sqlite
# The Duo Push Notification API configuration
duo_api:

View File

@ -22,7 +22,7 @@ session:
storage:
local:
path: /tmp/authelia/db.sqlite3
path: /var/lib/authelia/db.sqlite3
totp:
issuer: example.com

View File

@ -21,7 +21,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
path: /tmp/authelia/db.sqlite
path: /var/lib/authelia/db.sqlite
# TOTP Issuer Name
#

View File

@ -57,7 +57,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
path: /tmp/authelia/db.sqlite3
path: /var/lib/authelia/db.sqlite3
# TOTP Issuer Name
#

View File

@ -21,7 +21,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
path: /tmp/authelia/db.sqlite
path: /var/lib/authelia/db.sqlite
# Access Control
#

View File

@ -20,24 +20,14 @@ session:
inactivity: 5
expiration: 8
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
path: /tmp/authelia/db.sqlite
path: /var/lib/authelia/db.sqlite
# TOTP Issuer Name
#
# This will be the issuer name displayed in Google Authenticator
# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
totp:
issuer: example.com
# Access Control
#
# Access control is a set of rules you can use to restrict user access to certain
# resources.
access_control:
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
default_policy: deny
rules:
@ -70,37 +60,12 @@ access_control:
subject: "user:bob"
policy: two_factor
# Configuration of the authentication regulation mechanism.
regulation:
# Set it to 0 to disable max_retries.
max_retries: 3
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 3
# The length of time before a banned user can login again.
ban_time: 5
# Default redirection URL
#
# Note: this parameter is optional. If not provided, user won't
# be redirected upon successful authentication.
#default_redirection_url: https://authelia.example.domain
notifier:
# For testing purpose, notifications can be sent in a file
# filesystem:
# filename: /tmp/authelia/notification.txt
# Use your email account to send the notifications. You can use an app password.
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
## email:
## username: user@example.com
## password: yourpassword
## sender: admin@example.com
## service: gmail
# Use a SMTP server for sending notifications
smtp:
host: smtp
port: 1025

View File

@ -4,3 +4,4 @@ services:
volumes:
- "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml"
- "/tmp/authelia:/tmp/authelia"

View File

@ -20,7 +20,7 @@ session:
storage:
local:
path: /tmp/authelia/db.sqlite
path: /var/lib/authelia/db.sqlite
access_control:
default_policy: bypass

View File

@ -45,7 +45,10 @@ 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/secondfactor/available", 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("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

@ -7,8 +7,6 @@ import (
"testing"
"time"
"github.com/clems4ever/authelia/internal/storage"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -49,35 +47,6 @@ func (s *TwoFactorSuite) SetupTest() {
s.verifyIsHome(ctx, s.T())
}
func (s *TwoFactorSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
username := "john"
password := "password"
// Clean up any TOTP secret already in DB
provider := storage.NewSQLiteProvider("/tmp/authelia/db.sqlite3")
require.NoError(s.T(), provider.DeleteTOTPSecret(username))
// Login one factor
s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
// Check the user is asked to register a new device
s.WaitElementLocatedByClassName(ctx, s.T(), "state-not-registered")
// Then register the TOTP factor
s.doRegisterTOTP(ctx, s.T())
// And logout
s.doLogout(ctx, s.T())
// Login one factor again
s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
// now the user should be asked to perform 2FA
s.WaitElementLocatedByClassName(ctx, s.T(), "state-method")
}
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

View File

@ -49,15 +49,15 @@ func (s *HighAvailabilityWebDriverSuite) SetupTest() {
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
err := haDockerEnvironment.Restart("mariadb")
s.Assert().NoError(err)
s.Require().NoError(err)
time.Sleep(2 * time.Second)
time.Sleep(20 * time.Second)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
@ -229,6 +229,10 @@ func (s *HighAvailabilitySuite) TestHighAvailabilityWebDriverSuite() {
suite.Run(s.T(), NewHighAvailabilityWebDriverSuite())
}
func TestHighAvailabilityWebDriverSuite(t *testing.T) {
suite.Run(t, NewHighAvailabilityWebDriverSuite())
}
func TestHighAvailabilitySuite(t *testing.T) {
suite.Run(t, NewHighAvailabilitySuite())
}

View File

@ -53,7 +53,7 @@ func init() {
SetUpTimeout: 5 * time.Minute,
OnSetupTimeout: onSetupTimeout,
TearDown: teardown,
TestTimeout: 2 * time.Minute,
TestTimeout: 3 * time.Minute,
TearDownTimeout: 2 * time.Minute,
Description: `This suite is used to test Authelia in a standalone
configuration with in-memory sessions and a local sqlite db stored on disk`,

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/clems4ever/authelia/internal/storage"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -65,6 +67,35 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
}
func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
username := "john"
password := "password"
// Clean up any TOTP secret already in DB
provider := storage.NewSQLiteProvider("/tmp/authelia/db.sqlite3")
require.NoError(s.T(), provider.DeleteTOTPSecret(username))
// Login one factor
s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
// Check the user is asked to register a new device
s.WaitElementLocatedByClassName(ctx, s.T(), "state-not-registered")
// Then register the TOTP factor
s.doRegisterTOTP(ctx, s.T())
// And logout
s.doLogout(ctx, s.T())
// Login one factor again
s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
// now the user should be asked to perform 2FA
s.WaitElementLocatedByClassName(ctx, s.T(), "state-method")
}
type StandaloneSuite struct {
suite.Suite
}

13
web/package-lock.json generated
View File

@ -1566,6 +1566,14 @@
"@types/react": "*"
}
},
"@types/react-ga": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@types/react-ga/-/react-ga-2.3.0.tgz",
"integrity": "sha512-7Vkv6wH1Kem4vkjuJxRYxDgLfokm0shugDk0W5p9C28POrsPAXezLbgP5C2tyFZ7lKARdyrCxwkdRTC1UV0dHg==",
"requires": {
"react-ga": "*"
}
},
"@types/react-router": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz",
@ -11311,6 +11319,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz",
"integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw=="
},
"react-ga": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/react-ga/-/react-ga-2.7.0.tgz",
"integrity": "sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA=="
},
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",

View File

@ -18,6 +18,7 @@
"@types/query-string": "^6.3.0",
"@types/react": "16.9.12",
"@types/react-dom": "16.9.4",
"@types/react-ga": "^2.3.0",
"@types/react-router-dom": "^5.1.2",
"axios": "^0.19.0",
"chai": "^4.2.0",
@ -28,6 +29,7 @@
"query-string": "^6.9.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-ga": "^2.7.0",
"react-loading": "^2.0.3",
"react-otp-input": "^1.0.1",
"react-router-dom": "^5.1.2",

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
BrowserRouter as Router, Route, Switch, Redirect
} from "react-router-dom";
@ -17,36 +17,52 @@ 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 Tracker from "./components/Tracker";
import { useTracking } from "./hooks/Tracking";
const App: React.FC = () => {
const [notification, setNotification] = useState(null as Notification | null);
const [configuration, fetchConfig, , fetchConfigError] = useConfiguration();
const tracker = useTracking(configuration);
useEffect(() => {
if (fetchConfigError) {
console.error(fetchConfigError);
}
}, [fetchConfigError]);
useEffect(() => { fetchConfig() }, [fetchConfig]);
return (
<NotificationsContext.Provider value={{ notification, setNotification }} >
<NotificationBar onClose={() => setNotification(null)} />
<Router>
<Switch>
<Route path={ResetPasswordStep1Route} exact>
<ResetPasswordStep1 />
</Route>
<Route path={ResetPasswordStep2Route} exact>
<ResetPasswordStep2 />
</Route>
<Route path={RegisterSecurityKeyRoute} exact>
<RegisterSecurityKey />
</Route>
<Route path={RegisterOneTimePasswordRoute} exact>
<RegisterOneTimePassword />
</Route>
<Route path={LogoutRoute} exact>
<SignOut />
</Route>
<Route path={FirstFactorRoute}>
<LoginPortal />
</Route>
<Route path="/">
<Redirect to={FirstFactorRoute}></Redirect>
</Route>
</Switch>
<Tracker tracker={tracker}>
<NotificationBar onClose={() => setNotification(null)} />
<Switch>
<Route path={ResetPasswordStep1Route} exact>
<ResetPasswordStep1 />
</Route>
<Route path={ResetPasswordStep2Route} exact>
<ResetPasswordStep2 />
</Route>
<Route path={RegisterSecurityKeyRoute} exact>
<RegisterSecurityKey />
</Route>
<Route path={RegisterOneTimePasswordRoute} exact>
<RegisterOneTimePassword />
</Route>
<Route path={LogoutRoute} exact>
<SignOut />
</Route>
<Route path={FirstFactorRoute}>
<LoginPortal />
</Route>
<Route path="/">
<Redirect to={FirstFactorRoute}></Redirect>
</Route>
</Switch>
</Tracker>
</Router>
</NotificationsContext.Provider>
);

View File

@ -0,0 +1,11 @@
import React from 'react';
import { mount } from "enzyme";
import Tracker from "./Tracker";
import { MemoryRouter as Router } from 'react-router-dom';
const mountWithRouter = node => mount(<Router>{node}</Router>);
it('renders without crashing', () => {
mountWithRouter(<Tracker trackingIDs={[]} />);
});

View File

@ -0,0 +1,35 @@
import React, { useEffect, useCallback, Fragment, ReactNode } from "react";
import { useLocation } from "react-router";
import ReactGA, { Tracker } from "react-ga";
export interface Props {
tracker: Tracker | undefined;
children?: ReactNode;
}
export default function (props: Props) {
const location = useLocation();
const trackPage = useCallback((page: string) => {
if (props.tracker) {
ReactGA.set({ page });
ReactGA.pageview(page);
}
}, [props.tracker]);
useEffect(() => {
if (props.tracker) {
ReactGA.initialize([props.tracker]);
}
}, [props.tracker]);
useEffect(() => {
trackPage(location.pathname + location.search);
}, [trackPage, location.pathname, location.search]);
return (
<Fragment>
{props.children}
</Fragment>
)
}

View File

@ -1,6 +1,10 @@
import { useRemoteCall } from "./RemoteCall";
import { getAvailable2FAMethods } from "../services/Configuration";
import { getConfiguration, getExtendedConfiguration } from "../services/Configuration";
export function useAutheliaConfiguration() {
return useRemoteCall(getAvailable2FAMethods, []);
export function useConfiguration() {
return useRemoteCall(getConfiguration, []);
}
export function useExtendedConfiguration() {
return useRemoteCall(getExtendedConfiguration, []);
}

View File

@ -0,0 +1,15 @@
import { useState, useEffect } from "react";
import { Configuration } from "../models/Configuration";
import { Tracker } from "react-ga";
export function useTracking(configuration: Configuration | undefined) {
const [trackingIds, setTrackingIds] = useState(undefined as Tracker | undefined);
useEffect(() => {
if (configuration && configuration.ga_tracking_id) {
setTrackingIds({ trackingId: configuration.ga_tracking_id });
}
}, [configuration]);
return trackingIds;
}

View File

@ -1,3 +1,9 @@
import { SecondFactorMethod } from "./Methods";
export type Configuration = Set<SecondFactorMethod>
export interface Configuration {
ga_tracking_id: string;
}
export interface ExtendedConfiguration {
available_methods: Set<SecondFactorMethod>;
}

View File

@ -25,6 +25,9 @@ export const UserInfoPath = "/api/user/info";
export const UserInfo2FAMethodPath = "/api/user/info/2fa_method";
export const Available2FAMethodsPath = "/api/secondfactor/available";
export const ConfigurationPath = "/api/configuration";
export const ExtendedConfigurationPath = "/api/configuration/extended";
export interface ErrorResponse {
status: "KO";
message: string;

View File

@ -1,9 +1,17 @@
import { Get } from "./Client";
import { Available2FAMethodsPath } from "./Api";
import { Method2FA, toEnum } from "./UserPreferences";
import { Configuration } from "../models/Configuration";
import { ExtendedConfigurationPath, ConfigurationPath } from "./Api";
import { toEnum, Method2FA } from "./UserPreferences";
import { Configuration, ExtendedConfiguration } from "../models/Configuration";
export async function getAvailable2FAMethods(): Promise<Configuration> {
const methods = await Get<Method2FA[]>(Available2FAMethodsPath);
return new Set(methods.map(toEnum));
export async function getConfiguration(): Promise<Configuration> {
return Get<Configuration>(ConfigurationPath);
}
interface ExtendedConfigurationPayload {
available_methods: Method2FA[];
}
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
const config = await Get<ExtendedConfigurationPayload>(ExtendedConfigurationPath);
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
}

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 { useAutheliaConfiguration } from "../../hooks/Configuration";
import { useExtendedConfiguration } from "../../hooks/Configuration";
export default function () {
const history = useHistory();
@ -24,7 +24,7 @@ export default function () {
const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration();
const redirect = useCallback((url: string) => history.push(url), [history]);

View File

@ -18,7 +18,7 @@ import {
} from "../../../Routes";
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
import { UserInfo } from "../../../models/UserInfo";
import { Configuration } from "../../../models/Configuration";
import { ExtendedConfiguration } from "../../../models/Configuration";
import u2fApi from "u2f-api";
import { AuthenticationLevel } from "../../../services/State";
@ -29,7 +29,7 @@ export interface Props {
authenticationLevel: AuthenticationLevel;
userInfo: UserInfo;
configuration: Configuration;
configuration: ExtendedConfiguration;
onMethodChanged: (method: SecondFactorMethod) => void;
onAuthenticationSuccess: (redirectURL: string | undefined) => void;
@ -89,7 +89,7 @@ export default function (props: Props) {
showBrand>
<MethodSelectionDialog
open={methodSelectionOpen}
methods={props.configuration}
methods={props.configuration.available_methods}
u2fSupported={u2fSupported}
onClose={() => setMethodSelectionOpen(false)}
onClick={handleMethodSelected} />