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. # be redirected upon successful authentication.
default_redirection_url: https://home.example.com:8080/ 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 # TOTP Issuer Name
# #
# This will be the issuer name displayed in Google Authenticator # 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 && \ RUN addgroup --gid ${GROUP_ID} dev && \
adduser --uid ${USER_ID} -G dev -D dev adduser --uid ${USER_ID} -G dev -D dev
USER 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: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
volumes:
- "/tmp/authelia:/tmp/authelia"
environment: environment:
- ENVIRONMENT=dev - ENVIRONMENT=dev
restart: always restart: always

View File

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

View File

@ -4,9 +4,6 @@ set -x
go get github.com/cespare/reflex 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 # Sleep 10 seconds to wait the end of npm install updating web directory
# and making reflex reload multiple times. # and making reflex reload multiple times.
sleep 10 sleep 10

View File

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

View File

@ -2,10 +2,12 @@ package schema
// Configuration object extracted from YAML configuration file. // Configuration object extracted from YAML configuration file.
type Configuration struct { type Configuration struct {
Port int `yaml:"port"` Port int `yaml:"port"`
LogsLevel string `yaml:"logs_level"` LogsLevel string `yaml:"logs_level"`
JWTSecret string `yaml:"jwt_secret"` JWTSecret string `yaml:"jwt_secret"`
DefaultRedirectionURL string `yaml:"default_redirection_url"` DefaultRedirectionURL string `yaml:"default_redirection_url"`
GoogleAnalyticsTrackingID string `yaml:"google_analytics"`
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"` AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
Session SessionConfiguration `yaml:"session"` 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 { type SecondFactorAvailableMethodsFixture struct {
suite.Suite suite.Suite
mock *mocks.MockAutheliaCtx mock *mocks.MockAutheliaCtx
} }
@ -24,16 +23,22 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
SecondFactorAvailableMethodsGet(s.mock.Ctx) expectedBody := ExtendedConfigurationBody{
s.mock.Assert200OK(s.T(), []string{"totp", "u2f"}) AvailableMethods: []string{"totp", "u2f"},
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() { func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{}, DuoAPI: &schema.DuoAPIConfiguration{},
} }
SecondFactorAvailableMethodsGet(s.mock.Ctx) expectedBody := ExtendedConfigurationBody{
s.mock.Assert200OK(s.T(), []string{"totp", "u2f", "mobile_push"}) AvailableMethods: []string{"totp", "u2f", "mobile_push"},
}
ExtendedConfigurationGet(s.mock.Ctx)
s.mock.Assert200OK(s.T(), expectedBody)
} }
func TestRunSuite(t *testing.T) { 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/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.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost)) 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( router.POST("/api/reset-password", autheliaMiddleware(
handlers.ResetPasswordPost)) handlers.ResetPasswordPost))
// 2FA preferences and settings related endpoints.
router.GET("/api/secondfactor/available", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet)))
// Information about the user // Information about the user
router.GET("/api/user/info", autheliaMiddleware( router.GET("/api/user/info", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserInfoGet))) middlewares.RequireFirstFactor(handlers.UserInfoGet)))

View File

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

View File

@ -22,7 +22,7 @@ session:
storage: storage:
local: local:
path: /tmp/authelia/db.sqlite3 path: /var/lib/authelia/db.sqlite3
totp: totp:
issuer: example.com 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
local: local:
path: /tmp/authelia/db.sqlite path: /var/lib/authelia/db.sqlite
# TOTP Issuer Name # 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
local: local:
path: /tmp/authelia/db.sqlite3 path: /var/lib/authelia/db.sqlite3
# TOTP Issuer Name # 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 # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
local: local:
path: /tmp/authelia/db.sqlite path: /var/lib/authelia/db.sqlite
# Access Control # Access Control
# #

View File

@ -20,24 +20,14 @@ session:
inactivity: 5 inactivity: 5
expiration: 8 expiration: 8
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:
local: 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: totp:
issuer: example.com issuer: example.com
# Access Control
#
# Access control is a set of rules you can use to restrict user access to certain
# resources.
access_control: access_control:
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
default_policy: deny default_policy: deny
rules: rules:
@ -70,37 +60,12 @@ access_control:
subject: "user:bob" subject: "user:bob"
policy: two_factor policy: two_factor
# Configuration of the authentication regulation mechanism.
regulation: regulation:
# Set it to 0 to disable max_retries.
max_retries: 3 max_retries: 3
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 3 find_time: 3
# The length of time before a banned user can login again.
ban_time: 5 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: 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: smtp:
host: smtp host: smtp
port: 1025 port: 1025

View File

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

View File

@ -20,7 +20,7 @@ session:
storage: storage:
local: local:
path: /tmp/authelia/db.sqlite path: /var/lib/authelia/db.sqlite
access_control: access_control:
default_policy: bypass 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("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/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/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

@ -7,8 +7,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/clems4ever/authelia/internal/storage"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -49,35 +47,6 @@ func (s *TwoFactorSuite) SetupTest() {
s.verifyIsHome(ctx, s.T()) 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() { func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/clems4ever/authelia/internal/storage"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -65,6 +67,35 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon") 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 { type StandaloneSuite struct {
suite.Suite suite.Suite
} }

13
web/package-lock.json generated
View File

@ -1566,6 +1566,14 @@
"@types/react": "*" "@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": { "@types/react-router": {
"version": "5.1.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz", "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", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz",
"integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw==" "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": { "react-is": {
"version": "16.12.0", "version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", "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/query-string": "^6.3.0",
"@types/react": "16.9.12", "@types/react": "16.9.12",
"@types/react-dom": "16.9.4", "@types/react-dom": "16.9.4",
"@types/react-ga": "^2.3.0",
"@types/react-router-dom": "^5.1.2", "@types/react-router-dom": "^5.1.2",
"axios": "^0.19.0", "axios": "^0.19.0",
"chai": "^4.2.0", "chai": "^4.2.0",
@ -28,6 +29,7 @@
"query-string": "^6.9.0", "query-string": "^6.9.0",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-ga": "^2.7.0",
"react-loading": "^2.0.3", "react-loading": "^2.0.3",
"react-otp-input": "^1.0.1", "react-otp-input": "^1.0.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } 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,36 +17,52 @@ 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 Tracker from "./components/Tracker";
import { useTracking } from "./hooks/Tracking";
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();
const tracker = useTracking(configuration);
useEffect(() => {
if (fetchConfigError) {
console.error(fetchConfigError);
}
}, [fetchConfigError]);
useEffect(() => { fetchConfig() }, [fetchConfig]);
return ( return (
<NotificationsContext.Provider value={{ notification, setNotification }} > <NotificationsContext.Provider value={{ notification, setNotification }} >
<NotificationBar onClose={() => setNotification(null)} />
<Router> <Router>
<Switch> <Tracker tracker={tracker}>
<Route path={ResetPasswordStep1Route} exact> <NotificationBar onClose={() => setNotification(null)} />
<ResetPasswordStep1 /> <Switch>
</Route> <Route path={ResetPasswordStep1Route} exact>
<Route path={ResetPasswordStep2Route} exact> <ResetPasswordStep1 />
<ResetPasswordStep2 /> </Route>
</Route> <Route path={ResetPasswordStep2Route} exact>
<Route path={RegisterSecurityKeyRoute} exact> <ResetPasswordStep2 />
<RegisterSecurityKey /> </Route>
</Route> <Route path={RegisterSecurityKeyRoute} exact>
<Route path={RegisterOneTimePasswordRoute} exact> <RegisterSecurityKey />
<RegisterOneTimePassword /> </Route>
</Route> <Route path={RegisterOneTimePasswordRoute} exact>
<Route path={LogoutRoute} exact> <RegisterOneTimePassword />
<SignOut /> </Route>
</Route> <Route path={LogoutRoute} exact>
<Route path={FirstFactorRoute}> <SignOut />
<LoginPortal /> </Route>
</Route> <Route path={FirstFactorRoute}>
<Route path="/"> <LoginPortal />
<Redirect to={FirstFactorRoute}></Redirect> </Route>
</Route> <Route path="/">
</Switch> <Redirect to={FirstFactorRoute}></Redirect>
</Route>
</Switch>
</Tracker>
</Router> </Router>
</NotificationsContext.Provider> </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 { useRemoteCall } from "./RemoteCall";
import { getAvailable2FAMethods } from "../services/Configuration"; import { getConfiguration, getExtendedConfiguration } from "../services/Configuration";
export function useAutheliaConfiguration() { export function useConfiguration() {
return useRemoteCall(getAvailable2FAMethods, []); 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"; 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 UserInfo2FAMethodPath = "/api/user/info/2fa_method";
export const Available2FAMethodsPath = "/api/secondfactor/available"; export const Available2FAMethodsPath = "/api/secondfactor/available";
export const ConfigurationPath = "/api/configuration";
export const ExtendedConfigurationPath = "/api/configuration/extended";
export interface ErrorResponse { export interface ErrorResponse {
status: "KO"; status: "KO";
message: string; message: string;

View File

@ -1,9 +1,17 @@
import { Get } from "./Client"; import { Get } from "./Client";
import { Available2FAMethodsPath } from "./Api"; import { ExtendedConfigurationPath, ConfigurationPath } from "./Api";
import { Method2FA, toEnum } from "./UserPreferences"; import { toEnum, Method2FA } from "./UserPreferences";
import { Configuration } from "../models/Configuration"; import { Configuration, ExtendedConfiguration } from "../models/Configuration";
export async function getAvailable2FAMethods(): Promise<Configuration> { export async function getConfiguration(): Promise<Configuration> {
const methods = await Get<Method2FA[]>(Available2FAMethodsPath); return Get<Configuration>(ConfigurationPath);
return new Set(methods.map(toEnum)); }
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 { 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 { useAutheliaConfiguration } from "../../hooks/Configuration"; import { useExtendedConfiguration } from "../../hooks/Configuration";
export default function () { export default function () {
const history = useHistory(); const history = useHistory();
@ -24,7 +24,7 @@ export default function () {
const [state, fetchState, , fetchStateError] = useAutheliaState(); const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo(); const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration(); const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration();
const redirect = useCallback((url: string) => history.push(url), [history]); const redirect = useCallback((url: string) => history.push(url), [history]);

View File

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